mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
new file: server/api/os.ts
This commit is contained in:
parent
e1c3b9d745
commit
929d293e5f
18 changed files with 1387 additions and 26 deletions
|
|
@ -31,14 +31,15 @@ import AeThexOS from "@/pages/os";
|
||||||
import Network from "@/pages/network";
|
import Network from "@/pages/network";
|
||||||
import NetworkProfile from "@/pages/network-profile";
|
import NetworkProfile from "@/pages/network-profile";
|
||||||
import Lab from "@/pages/lab";
|
import Lab from "@/pages/lab";
|
||||||
import Projects from "@/pages/projects";
|
import HubProjects from "@/pages/hub/projects";
|
||||||
import Messaging from "@/pages/messaging";
|
import HubMessaging from "@/pages/hub/messaging";
|
||||||
import Marketplace from "@/pages/marketplace";
|
import HubMarketplace from "@/pages/hub/marketplace";
|
||||||
import Settings from "@/pages/settings";
|
import HubSettings from "@/pages/hub/settings";
|
||||||
import FileManager from "@/pages/file-manager";
|
import HubFileManager from "@/pages/hub/file-manager";
|
||||||
import CodeGallery from "@/pages/code-gallery";
|
import HubCodeGallery from "@/pages/hub/code-gallery";
|
||||||
import Notifications from "@/pages/notifications";
|
import HubNotifications from "@/pages/hub/notifications";
|
||||||
import Analytics from "@/pages/analytics";
|
import HubAnalytics from "@/pages/hub/analytics";
|
||||||
|
import OsLink from "@/pages/os/link";
|
||||||
import { LabTerminalProvider } from "@/hooks/use-lab-terminal";
|
import { LabTerminalProvider } from "@/hooks/use-lab-terminal";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
|
|
@ -67,17 +68,18 @@ function Router() {
|
||||||
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
|
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
|
||||||
<Route path="/pitch" component={Pitch} />
|
<Route path="/pitch" component={Pitch} />
|
||||||
<Route path="/os" component={AeThexOS} />
|
<Route path="/os" component={AeThexOS} />
|
||||||
|
<Route path="/os/link">{() => <ProtectedRoute><OsLink /></ProtectedRoute>}</Route>
|
||||||
<Route path="/network" component={Network} />
|
<Route path="/network" component={Network} />
|
||||||
<Route path="/network/:slug" component={NetworkProfile} />
|
<Route path="/network/:slug" component={NetworkProfile} />
|
||||||
<Route path="/lab" component={Lab} />
|
<Route path="/lab" component={Lab} />
|
||||||
<Route path="/projects">{() => <ProtectedRoute><Projects /></ProtectedRoute>}</Route>
|
<Route path="/hub/projects">{() => <ProtectedRoute><HubProjects /></ProtectedRoute>}</Route>
|
||||||
<Route path="/messaging">{() => <ProtectedRoute><Messaging /></ProtectedRoute>}</Route>
|
<Route path="/hub/messaging">{() => <ProtectedRoute><HubMessaging /></ProtectedRoute>}</Route>
|
||||||
<Route path="/marketplace">{() => <ProtectedRoute><Marketplace /></ProtectedRoute>}</Route>
|
<Route path="/hub/marketplace">{() => <ProtectedRoute><HubMarketplace /></ProtectedRoute>}</Route>
|
||||||
<Route path="/settings">{() => <ProtectedRoute><Settings /></ProtectedRoute>}</Route>
|
<Route path="/hub/settings">{() => <ProtectedRoute><HubSettings /></ProtectedRoute>}</Route>
|
||||||
<Route path="/file-manager">{() => <ProtectedRoute><FileManager /></ProtectedRoute>}</Route>
|
<Route path="/hub/file-manager">{() => <ProtectedRoute><HubFileManager /></ProtectedRoute>}</Route>
|
||||||
<Route path="/code-gallery">{() => <ProtectedRoute><CodeGallery /></ProtectedRoute>}</Route>
|
<Route path="/hub/code-gallery">{() => <ProtectedRoute><HubCodeGallery /></ProtectedRoute>}</Route>
|
||||||
<Route path="/notifications">{() => <ProtectedRoute><Notifications /></ProtectedRoute>}</Route>
|
<Route path="/hub/notifications">{() => <ProtectedRoute><HubNotifications /></ProtectedRoute>}</Route>
|
||||||
<Route path="/analytics">{() => <ProtectedRoute><Analytics /></ProtectedRoute>}</Route>
|
<Route path="/hub/analytics">{() => <ProtectedRoute><HubAnalytics /></ProtectedRoute>}</Route>
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
216
client/src/pages/os/link.tsx
Normal file
216
client/src/pages/os/link.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Github, Globe, MessageSquare, Loader2 } from "lucide-react";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
|
export default function OsLink() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [linkedIdentities, setLinkedIdentities] = useState<
|
||||||
|
Array<{ provider: string; external_id: string; verified_at: string }>
|
||||||
|
>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
{ name: "Roblox", id: "roblox", icon: Globe, color: "text-red-500" },
|
||||||
|
{ name: "Discord", id: "discord", icon: MessageSquare, color: "text-indigo-500" },
|
||||||
|
{ name: "GitHub", id: "github", icon: Github, color: "text-gray-300" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleLinkStart = async (provider: string) => {
|
||||||
|
if (!user?.id) {
|
||||||
|
alert("Please log in first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/os/link/start", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "x-user-id": user.id },
|
||||||
|
body: JSON.stringify({ provider }),
|
||||||
|
});
|
||||||
|
const { redirect_url } = await res.json();
|
||||||
|
// In production, redirect to OAuth flow
|
||||||
|
alert(`Would redirect to: ${redirect_url}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Link failed:", error);
|
||||||
|
alert("Failed to start linking");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnlink = async (provider: string) => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch("/api/os/link/unlink", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "x-user-id": user.id },
|
||||||
|
body: JSON.stringify({ provider }),
|
||||||
|
});
|
||||||
|
setLinkedIdentities(linkedIdentities.filter((id) => id.provider !== provider));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unlink failed:", error);
|
||||||
|
alert("Failed to unlink identity");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-8">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<h1 className="text-4xl font-bold text-white mb-2">Identity Linking</h1>
|
||||||
|
<p className="text-gray-300 text-lg">
|
||||||
|
Link your accounts to get verified credentials across the AeThex ecosystem.
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-4">
|
||||||
|
💡 <strong>What this means:</strong> Your proofs are portable. Link once, use everywhere.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Providers */}
|
||||||
|
<div className="space-y-3 mb-12">
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-4">Available Platforms</h2>
|
||||||
|
{providers.map((provider) => {
|
||||||
|
const Icon = provider.icon;
|
||||||
|
const isLinked = linkedIdentities.some((id) => id.provider === provider.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={provider.id} className="bg-slate-800/50 border-slate-700 hover:border-slate-600 transition">
|
||||||
|
<CardContent className="flex items-center justify-between p-5">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Icon className={`w-7 h-7 ${provider.color}`} />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">{provider.name}</p>
|
||||||
|
{isLinked && (
|
||||||
|
<p className="text-sm text-green-400">
|
||||||
|
✓ Linked and verified
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
isLinked ? handleUnlink(provider.id) : handleLinkStart(provider.id)
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
variant={isLinked ? "outline" : "default"}
|
||||||
|
className={isLinked ? "border-red-500 text-red-500 hover:bg-red-500/10" : ""}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Linking...
|
||||||
|
</>
|
||||||
|
) : isLinked ? (
|
||||||
|
"Unlink"
|
||||||
|
) : (
|
||||||
|
"Link"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Cards */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<Card className="bg-cyan-900/20 border-cyan-700/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-cyan-400 text-lg flex items-center gap-2">
|
||||||
|
<span>🔐</span> Your Privacy
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-gray-300">
|
||||||
|
We never share your linked identities without your consent. Each platform only sees what you allow.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-green-900/20 border-green-700/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-green-400 text-lg flex items-center gap-2">
|
||||||
|
<span>✓</span> Verified Proofs
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-gray-300">
|
||||||
|
When you link, we create cryptographically signed proofs of your achievements that you can share.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-purple-900/20 border-purple-700/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-purple-400 text-lg flex items-center gap-2">
|
||||||
|
<span>🔗</span> Portable
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-gray-300">
|
||||||
|
Use your verified credentials across any platform that trusts AeThex, without creating new accounts.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-blue-900/20 border-blue-700/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-blue-400 text-lg flex items-center gap-2">
|
||||||
|
<span>🚪</span> Exit Path
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-gray-300">
|
||||||
|
If AeThex disappears, your proofs remain valid and your linked accounts are still yours.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<Card className="mt-8 bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">How It Works</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 text-sm text-gray-300">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-cyan-500 text-white flex items-center justify-center text-xs font-bold">1</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">Link Your Account</p>
|
||||||
|
<p className="text-gray-400">Connect your Roblox, Discord, or GitHub account securely.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-cyan-500 text-white flex items-center justify-center text-xs font-bold">2</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">Verify Ownership</p>
|
||||||
|
<p className="text-gray-400">We confirm you own the account (OAuth, challenge, etc).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-cyan-500 text-white flex items-center justify-center text-xs font-bold">3</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">Get Verified Proofs</p>
|
||||||
|
<p className="text-gray-400">Your achievements are signed and portable across platforms.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-cyan-500 text-white flex items-center justify-center text-xs font-bold">4</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">Use Everywhere</p>
|
||||||
|
<p className="text-gray-400">Share your proofs with any platform that trusts AeThex OS.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="mt-12 text-center text-sm text-gray-400">
|
||||||
|
<p>
|
||||||
|
💡 <strong>OS attests; platforms decide.</strong>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">We verify your credentials. Other platforms decide what access to grant.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -148,20 +148,19 @@ CREATE TABLE IF NOT EXISTS "projects" (
|
||||||
-- Create indexes for better query performance
|
-- Create indexes for better query performance
|
||||||
CREATE INDEX IF NOT EXISTS "messages_sender_id_idx" ON "messages" ("sender_id");
|
CREATE INDEX IF NOT EXISTS "messages_sender_id_idx" ON "messages" ("sender_id");
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "messages_recipient_id_idx" ON "messages" ("recipient_id");
|
-- Removed: marketplace_listings_seller_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "marketplace_listings_seller_id_idx" ON "marketplace_listings" ("seller_id");
|
-- Removed: marketplace_listings_category_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "marketplace_listings_category_idx" ON "marketplace_listings" ("category");
|
-- Removed: files_user_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "files_user_id_idx" ON "files" ("user_id");
|
-- Removed: files_parent_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "files_parent_id_idx" ON "files" ("parent_id");
|
-- Removed: notifications_user_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "notifications_user_id_idx" ON "notifications" ("user_id");
|
-- Removed: user_analytics_user_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "user_analytics_user_id_idx" ON "user_analytics" ("user_id");
|
-- Removed: code_gallery_creator_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "code_gallery_creator_id_idx" ON "code_gallery" ("creator_id");
|
-- Removed: projects_user_id_idx (schema mismatch)
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "projects_user_id_idx" ON "projects" ("user_id");
|
|
||||||
|
|
|
||||||
111
migrations/0002_os_kernel.sql
Normal file
111
migrations/0002_os_kernel.sql
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
-- AeThex OS Kernel Schema
|
||||||
|
-- Portable proof system for the entire ecosystem
|
||||||
|
-- This is the spine: identity coordination + entitlements + verification
|
||||||
|
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_subjects" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_subject_identities" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"subject_id" varchar NOT NULL REFERENCES "aethex_subjects"("id") ON DELETE CASCADE,
|
||||||
|
"provider" varchar NOT NULL,
|
||||||
|
"external_id" varchar NOT NULL,
|
||||||
|
"external_username" varchar,
|
||||||
|
"verified_at" timestamp,
|
||||||
|
"revoked_at" timestamp,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
CONSTRAINT "aethex_subject_identities_provider_external_id_unique" UNIQUE("provider", "external_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_issuers" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"name" varchar NOT NULL,
|
||||||
|
"issuer_class" varchar NOT NULL,
|
||||||
|
"scopes" json DEFAULT '[]'::json,
|
||||||
|
"public_key" text NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true,
|
||||||
|
"metadata" json DEFAULT '{}'::json,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_issuer_keys" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"issuer_id" varchar NOT NULL REFERENCES "aethex_issuers"("id") ON DELETE CASCADE,
|
||||||
|
"public_key" text NOT NULL,
|
||||||
|
"private_key_hash" text,
|
||||||
|
"is_active" boolean DEFAULT true,
|
||||||
|
"rotated_at" timestamp,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_entitlements" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"issuer_id" varchar NOT NULL REFERENCES "aethex_issuers"("id") ON DELETE CASCADE,
|
||||||
|
"subject_id" varchar REFERENCES "aethex_subjects"("id") ON DELETE CASCADE,
|
||||||
|
"external_subject_ref" varchar,
|
||||||
|
"schema_version" varchar DEFAULT 'v0.1',
|
||||||
|
"scope" varchar NOT NULL,
|
||||||
|
"entitlement_type" varchar NOT NULL,
|
||||||
|
"data" json NOT NULL,
|
||||||
|
"status" varchar DEFAULT 'active',
|
||||||
|
"signature" text,
|
||||||
|
"evidence_hash" varchar,
|
||||||
|
"issued_by_subject_id" varchar,
|
||||||
|
"expires_at" timestamp,
|
||||||
|
"revoked_at" timestamp,
|
||||||
|
"revocation_reason" text,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_entitlement_events" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"entitlement_id" varchar NOT NULL REFERENCES "aethex_entitlements"("id") ON DELETE CASCADE,
|
||||||
|
"event_type" varchar NOT NULL,
|
||||||
|
"actor_id" varchar,
|
||||||
|
"actor_type" varchar NOT NULL,
|
||||||
|
"reason" text,
|
||||||
|
"metadata" json DEFAULT '{}'::json,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "aethex_audit_log" (
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
"action" varchar NOT NULL,
|
||||||
|
"actor_id" varchar,
|
||||||
|
"actor_type" varchar NOT NULL,
|
||||||
|
"resource_type" varchar NOT NULL,
|
||||||
|
"resource_id" varchar NOT NULL,
|
||||||
|
"changes" json DEFAULT '{}'::json,
|
||||||
|
"ip_address" varchar,
|
||||||
|
"user_agent" text,
|
||||||
|
"status" varchar DEFAULT 'success',
|
||||||
|
"error_message" text,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
-- OS Indexes for performance
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_subject_identities_subject_id_idx" ON "aethex_subject_identities" ("subject_id");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_subject_identities_provider_external_id_idx" ON "aethex_subject_identities" ("provider", "external_id");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_issuer_keys_issuer_id_idx" ON "aethex_issuer_keys" ("issuer_id");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_entitlements_issuer_id_idx" ON "aethex_entitlements" ("issuer_id");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_entitlements_subject_id_idx" ON "aethex_entitlements" ("subject_id");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_entitlements_external_subject_ref_idx" ON "aethex_entitlements" ("external_subject_ref");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_entitlements_status_idx" ON "aethex_entitlements" ("status");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_entitlement_events_entitlement_id_idx" ON "aethex_entitlement_events" ("entitlement_id");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_audit_log_action_idx" ON "aethex_audit_log" ("action");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "aethex_audit_log_resource_type_resource_id_idx" ON "aethex_audit_log" ("resource_type", "resource_id");
|
||||||
|
--> statement-breakpoint
|
||||||
54
script/check-tables.ts
Normal file
54
script/check-tables.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { supabase } from "./client/src/lib/supabase";
|
||||||
|
|
||||||
|
async function checkTables() {
|
||||||
|
try {
|
||||||
|
// Check Hub tables
|
||||||
|
const hubTables = [
|
||||||
|
"messages",
|
||||||
|
"marketplace_listings",
|
||||||
|
"workspace_settings",
|
||||||
|
"files",
|
||||||
|
"notifications",
|
||||||
|
"user_analytics",
|
||||||
|
"code_gallery",
|
||||||
|
"documentation",
|
||||||
|
"custom_apps",
|
||||||
|
"projects",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check OS kernel tables
|
||||||
|
const osTables = [
|
||||||
|
"aethex_subjects",
|
||||||
|
"aethex_subject_identities",
|
||||||
|
"aethex_issuers",
|
||||||
|
"aethex_issuer_keys",
|
||||||
|
"aethex_entitlements",
|
||||||
|
"aethex_entitlement_events",
|
||||||
|
"aethex_audit_log",
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log("🔍 Checking Hub tables...");
|
||||||
|
for (const table of hubTables) {
|
||||||
|
const { error } = await supabase.from(table).select("*").limit(0);
|
||||||
|
if (error) {
|
||||||
|
console.log(` ❌ ${table} - NOT CREATED`);
|
||||||
|
} else {
|
||||||
|
console.log(` ✅ ${table}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n🔍 Checking OS Kernel tables...");
|
||||||
|
for (const table of osTables) {
|
||||||
|
const { error } = await supabase.from(table).select("*").limit(0);
|
||||||
|
if (error) {
|
||||||
|
console.log(` ❌ ${table} - NOT CREATED`);
|
||||||
|
} else {
|
||||||
|
console.log(` ✅ ${table}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTables();
|
||||||
53
script/migrate-os.ts
Normal file
53
script/migrate-os.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import pkg from "pg";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const { Client } = pkg;
|
||||||
|
|
||||||
|
async function runOSMigration() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Connecting to database...");
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ Connected to database");
|
||||||
|
|
||||||
|
const migrationSQL = readFileSync("./migrations/0002_os_kernel.sql", "utf-8");
|
||||||
|
const statements = migrationSQL
|
||||||
|
.split("--> statement-breakpoint")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0 && !s.startsWith("--"));
|
||||||
|
|
||||||
|
console.log(`\nExecuting ${statements.length} statements...`);
|
||||||
|
|
||||||
|
for (let i = 0; i < statements.length; i++) {
|
||||||
|
try {
|
||||||
|
await client.query(statements[i]);
|
||||||
|
console.log(`✓ Statement ${i + 1}/${statements.length} executed`);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.message.includes("already exists")) {
|
||||||
|
console.log(`⚠ Statement ${i + 1} skipped (already exists)`);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ Statement ${i + 1} failed: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✅ OS Kernel migration completed successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("\n❌ Migration failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runOSMigration();
|
||||||
56
script/run-os-migration.ts
Normal file
56
script/run-os-migration.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import pkg from "pg";
|
||||||
|
const { Client } = pkg;
|
||||||
|
|
||||||
|
async function runOSMigration() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("Connected to database");
|
||||||
|
|
||||||
|
// Read the OS kernel migration file
|
||||||
|
const migrationPath = join(process.cwd(), "migrations", "0002_os_kernel.sql");
|
||||||
|
const migrationSQL = readFileSync(migrationPath, "utf-8");
|
||||||
|
|
||||||
|
// Split by statement-breakpoint
|
||||||
|
const statements = migrationSQL
|
||||||
|
.split("--> statement-breakpoint")
|
||||||
|
.map((stmt) => stmt.trim())
|
||||||
|
.filter((stmt) => stmt.length > 0 && !stmt.startsWith("--"));
|
||||||
|
|
||||||
|
console.log(`Executing ${statements.length} statements...`);
|
||||||
|
|
||||||
|
for (let i = 0; i < statements.length; i++) {
|
||||||
|
try {
|
||||||
|
await client.query(statements[i]);
|
||||||
|
console.log(`✓ Statement ${i + 1}/${statements.length} executed`);
|
||||||
|
} catch (err: any) {
|
||||||
|
// Only fail on actual errors, not "already exists" type errors
|
||||||
|
if (
|
||||||
|
err.message.includes("already exists") ||
|
||||||
|
err.message.includes("UNIQUE constraint failed")
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`⚠ Statement ${i + 1}/${statements.length} skipped (${err.message})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ Statement ${i + 1} failed: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✅ OS Kernel migration completed successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("\n❌ Migration failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runOSMigration();
|
||||||
406
server/api/os.ts
Normal file
406
server/api/os.ts
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import { supabase } from "@/lib/supabase";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/os/link/start
|
||||||
|
* Begin identity linking flow
|
||||||
|
*/
|
||||||
|
router.post("/link/start", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { provider } = req.body;
|
||||||
|
const userId = req.headers["x-user-id"] as string;
|
||||||
|
|
||||||
|
if (!provider || !userId) {
|
||||||
|
return res.status(400).json({ error: "Missing provider or user" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkingSession = {
|
||||||
|
id: `link_${Date.now()}`,
|
||||||
|
state: Math.random().toString(36).substring(7),
|
||||||
|
expires_at: new Date(Date.now() + 10 * 60 * 1000),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
linking_session_id: linkingSession.id,
|
||||||
|
state: linkingSession.state,
|
||||||
|
redirect_url: `/os/link/redirect?provider=${provider}&state=${linkingSession.state}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Link start error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to start linking" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/os/link/complete
|
||||||
|
* Complete identity linking
|
||||||
|
*/
|
||||||
|
router.post("/link/complete", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { provider, external_id, external_username } = req.body;
|
||||||
|
const userId = req.headers["x-user-id"] as string;
|
||||||
|
|
||||||
|
if (!provider || !external_id || !userId) {
|
||||||
|
return res.status(400).json({ error: "Missing required fields" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update subject identity
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_subject_identities")
|
||||||
|
.upsert(
|
||||||
|
{
|
||||||
|
provider,
|
||||||
|
external_id,
|
||||||
|
external_username,
|
||||||
|
verified_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onConflict: "provider,external_id",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Log audit event
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "link_identity",
|
||||||
|
actor_id: userId,
|
||||||
|
actor_type: "user",
|
||||||
|
resource_type: "subject_identity",
|
||||||
|
resource_id: data?.[0]?.id || "unknown",
|
||||||
|
changes: { provider, external_id },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
identity: {
|
||||||
|
provider,
|
||||||
|
external_id,
|
||||||
|
verified_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Link complete error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to complete linking" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/os/link/unlink
|
||||||
|
* Remove identity link
|
||||||
|
*/
|
||||||
|
router.post("/link/unlink", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { provider, external_id } = req.body;
|
||||||
|
const userId = req.headers["x-user-id"] as string;
|
||||||
|
|
||||||
|
if (!provider || !external_id) {
|
||||||
|
return res.status(400).json({ error: "Missing provider or external_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_subject_identities")
|
||||||
|
.update({ revoked_at: new Date().toISOString() })
|
||||||
|
.match({ provider, external_id })
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "unlink_identity",
|
||||||
|
actor_id: userId,
|
||||||
|
actor_type: "user",
|
||||||
|
resource_type: "subject_identity",
|
||||||
|
resource_id: data?.[0]?.id || "unknown",
|
||||||
|
changes: { revoked: true },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: "Identity unlinked" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unlink error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to unlink identity" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/os/entitlements/issue
|
||||||
|
* Issue new entitlement (authorized issuers only)
|
||||||
|
*/
|
||||||
|
router.post("/entitlements/issue", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const issuerId = req.headers["x-issuer-id"] as string;
|
||||||
|
const {
|
||||||
|
subject_id,
|
||||||
|
external_subject_ref,
|
||||||
|
entitlement_type,
|
||||||
|
scope,
|
||||||
|
data,
|
||||||
|
expires_at,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!issuerId || (!subject_id && !external_subject_ref)) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Missing issuer_id or subject reference" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: entitlement, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.insert({
|
||||||
|
issuer_id: issuerId,
|
||||||
|
subject_id: subject_id || null,
|
||||||
|
external_subject_ref: external_subject_ref || null,
|
||||||
|
entitlement_type,
|
||||||
|
scope,
|
||||||
|
data: data || {},
|
||||||
|
status: "active",
|
||||||
|
expires_at: expires_at || null,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "issue_entitlement",
|
||||||
|
actor_id: issuerId,
|
||||||
|
actor_type: "issuer",
|
||||||
|
resource_type: "entitlement",
|
||||||
|
resource_id: entitlement?.id || "unknown",
|
||||||
|
changes: { entitlement_type, scope },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
entitlement: {
|
||||||
|
id: entitlement?.id,
|
||||||
|
type: entitlement_type,
|
||||||
|
scope,
|
||||||
|
created_at: entitlement?.created_at,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Issue error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to issue entitlement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/os/entitlements/verify
|
||||||
|
* Verify entitlement authenticity
|
||||||
|
*/
|
||||||
|
router.post("/entitlements/verify", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { entitlement_id } = req.body;
|
||||||
|
|
||||||
|
if (!entitlement_id) {
|
||||||
|
return res.status(400).json({ error: "Missing entitlement_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: entitlement, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.select("*, issuer:aethex_issuers(*)")
|
||||||
|
.eq("id", entitlement_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !entitlement) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ valid: false, reason: "Entitlement not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitlement.status === "revoked") {
|
||||||
|
return res.json({
|
||||||
|
valid: false,
|
||||||
|
reason: "revoked",
|
||||||
|
revoked_at: entitlement.revoked_at,
|
||||||
|
revocation_reason: entitlement.revocation_reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entitlement.status === "expired" ||
|
||||||
|
(entitlement.expires_at && new Date() > new Date(entitlement.expires_at))
|
||||||
|
) {
|
||||||
|
return res.json({
|
||||||
|
valid: false,
|
||||||
|
reason: "expired",
|
||||||
|
expires_at: entitlement.expires_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log verification event
|
||||||
|
await supabase.from("aethex_entitlement_events").insert({
|
||||||
|
entitlement_id,
|
||||||
|
event_type: "verified",
|
||||||
|
actor_type: "system",
|
||||||
|
reason: "API verification",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
valid: true,
|
||||||
|
entitlement: {
|
||||||
|
id: entitlement.id,
|
||||||
|
type: entitlement.entitlement_type,
|
||||||
|
scope: entitlement.scope,
|
||||||
|
data: entitlement.data,
|
||||||
|
issuer: {
|
||||||
|
id: entitlement.issuer?.id,
|
||||||
|
name: entitlement.issuer?.name,
|
||||||
|
class: entitlement.issuer?.issuer_class,
|
||||||
|
},
|
||||||
|
issued_at: entitlement.created_at,
|
||||||
|
expires_at: entitlement.expires_at,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Verify error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to verify entitlement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/os/entitlements/resolve
|
||||||
|
* Resolve entitlements by platform identity
|
||||||
|
*/
|
||||||
|
router.get("/entitlements/resolve", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { platform, id, subject_id } = req.query;
|
||||||
|
|
||||||
|
let entitlements: any[] = [];
|
||||||
|
|
||||||
|
if (subject_id) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.select("*, issuer:aethex_issuers(*)")
|
||||||
|
.eq("subject_id", subject_id as string)
|
||||||
|
.eq("status", "active");
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
entitlements = data || [];
|
||||||
|
} else if (platform && id) {
|
||||||
|
const externalRef = `${platform}:${id}`;
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.select("*, issuer:aethex_issuers(*)")
|
||||||
|
.eq("external_subject_ref", externalRef)
|
||||||
|
.eq("status", "active");
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
entitlements = data || [];
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: "Missing platform/id or subject_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
entitlements: entitlements.map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
type: e.entitlement_type,
|
||||||
|
scope: e.scope,
|
||||||
|
data: e.data,
|
||||||
|
issuer: {
|
||||||
|
name: e.issuer?.name,
|
||||||
|
class: e.issuer?.issuer_class,
|
||||||
|
},
|
||||||
|
issued_at: e.created_at,
|
||||||
|
expires_at: e.expires_at,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Resolve error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to resolve entitlements" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/os/entitlements/revoke
|
||||||
|
* Revoke entitlement
|
||||||
|
*/
|
||||||
|
router.post("/entitlements/revoke", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const issuerId = req.headers["x-issuer-id"] as string;
|
||||||
|
const { entitlement_id, reason } = req.body;
|
||||||
|
|
||||||
|
if (!entitlement_id || !reason) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Missing entitlement_id or reason" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.update({
|
||||||
|
status: "revoked",
|
||||||
|
revoked_at: new Date().toISOString(),
|
||||||
|
revocation_reason: reason,
|
||||||
|
})
|
||||||
|
.eq("id", entitlement_id)
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_entitlement_events").insert({
|
||||||
|
entitlement_id,
|
||||||
|
event_type: "revoked",
|
||||||
|
actor_id: issuerId,
|
||||||
|
actor_type: "issuer",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "revoke_entitlement",
|
||||||
|
actor_id: issuerId,
|
||||||
|
actor_type: "issuer",
|
||||||
|
resource_type: "entitlement",
|
||||||
|
resource_id: entitlement_id,
|
||||||
|
changes: { status: "revoked", reason },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: "Entitlement revoked" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Revoke error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to revoke entitlement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/os/issuers/:id
|
||||||
|
* Get issuer metadata
|
||||||
|
*/
|
||||||
|
router.get("/issuers/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const { data: issuer, error } = await supabase
|
||||||
|
.from("aethex_issuers")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !issuer) {
|
||||||
|
return res.status(404).json({ error: "Issuer not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: issuer.id,
|
||||||
|
name: issuer.name,
|
||||||
|
class: issuer.issuer_class,
|
||||||
|
scopes: issuer.scopes,
|
||||||
|
public_key: issuer.public_key,
|
||||||
|
is_active: issuer.is_active,
|
||||||
|
metadata: issuer.metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Issuer fetch error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch issuer" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
368
server/routes.ts
368
server/routes.ts
|
|
@ -749,5 +749,373 @@ export async function registerRoutes(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== OS KERNEL ROUTES ==========
|
||||||
|
// Identity Linking
|
||||||
|
app.post("/api/os/link/start", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { provider } = req.body;
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
|
if (!provider || !userId) {
|
||||||
|
return res.status(400).json({ error: "Missing provider or user" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkingSession = {
|
||||||
|
id: `link_${Date.now()}`,
|
||||||
|
state: Math.random().toString(36).substring(7),
|
||||||
|
expires_at: new Date(Date.now() + 10 * 60 * 1000),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
linking_session_id: linkingSession.id,
|
||||||
|
state: linkingSession.state,
|
||||||
|
redirect_url: `/os/link/redirect?provider=${provider}&state=${linkingSession.state}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Link start error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to start linking" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/os/link/complete", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { provider, external_id, external_username } = req.body;
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
|
if (!provider || !external_id || !userId) {
|
||||||
|
return res.status(400).json({ error: "Missing required fields" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_subject_identities")
|
||||||
|
.upsert(
|
||||||
|
{
|
||||||
|
provider,
|
||||||
|
external_id,
|
||||||
|
external_username,
|
||||||
|
verified_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onConflict: "provider,external_id",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "link_identity",
|
||||||
|
actor_id: userId,
|
||||||
|
actor_type: "user",
|
||||||
|
resource_type: "subject_identity",
|
||||||
|
resource_id: data?.[0]?.id || "unknown",
|
||||||
|
changes: { provider, external_id },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
identity: {
|
||||||
|
provider,
|
||||||
|
external_id,
|
||||||
|
verified_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Link complete error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to complete linking" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/os/link/unlink", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { provider, external_id } = req.body;
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
|
if (!provider || !external_id) {
|
||||||
|
return res.status(400).json({ error: "Missing provider or external_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_subject_identities")
|
||||||
|
.update({ revoked_at: new Date().toISOString() })
|
||||||
|
.match({ provider, external_id })
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "unlink_identity",
|
||||||
|
actor_id: userId,
|
||||||
|
actor_type: "user",
|
||||||
|
resource_type: "subject_identity",
|
||||||
|
resource_id: data?.[0]?.id || "unknown",
|
||||||
|
changes: { revoked: true },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: "Identity unlinked" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unlink error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to unlink identity" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entitlements
|
||||||
|
app.post("/api/os/entitlements/issue", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const issuerId = req.headers["x-issuer-id"] as string;
|
||||||
|
const {
|
||||||
|
subject_id,
|
||||||
|
external_subject_ref,
|
||||||
|
entitlement_type,
|
||||||
|
scope,
|
||||||
|
data,
|
||||||
|
expires_at,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!issuerId || (!subject_id && !external_subject_ref)) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Missing issuer_id or subject reference" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: entitlement, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.insert({
|
||||||
|
issuer_id: issuerId,
|
||||||
|
subject_id: subject_id || null,
|
||||||
|
external_subject_ref: external_subject_ref || null,
|
||||||
|
entitlement_type,
|
||||||
|
scope,
|
||||||
|
data: data || {},
|
||||||
|
status: "active",
|
||||||
|
expires_at: expires_at || null,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "issue_entitlement",
|
||||||
|
actor_id: issuerId,
|
||||||
|
actor_type: "issuer",
|
||||||
|
resource_type: "entitlement",
|
||||||
|
resource_id: entitlement?.id || "unknown",
|
||||||
|
changes: { entitlement_type, scope },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
entitlement: {
|
||||||
|
id: entitlement?.id,
|
||||||
|
type: entitlement_type,
|
||||||
|
scope,
|
||||||
|
created_at: entitlement?.created_at,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Issue error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to issue entitlement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/os/entitlements/verify", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { entitlement_id } = req.body;
|
||||||
|
|
||||||
|
if (!entitlement_id) {
|
||||||
|
return res.status(400).json({ error: "Missing entitlement_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: entitlement, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.select("*, issuer:aethex_issuers(*)")
|
||||||
|
.eq("id", entitlement_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !entitlement) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ valid: false, reason: "Entitlement not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitlement.status === "revoked") {
|
||||||
|
return res.json({
|
||||||
|
valid: false,
|
||||||
|
reason: "revoked",
|
||||||
|
revoked_at: entitlement.revoked_at,
|
||||||
|
revocation_reason: entitlement.revocation_reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entitlement.status === "expired" ||
|
||||||
|
(entitlement.expires_at && new Date() > new Date(entitlement.expires_at))
|
||||||
|
) {
|
||||||
|
return res.json({
|
||||||
|
valid: false,
|
||||||
|
reason: "expired",
|
||||||
|
expires_at: entitlement.expires_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase.from("aethex_entitlement_events").insert({
|
||||||
|
entitlement_id,
|
||||||
|
event_type: "verified",
|
||||||
|
actor_type: "system",
|
||||||
|
reason: "API verification",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
valid: true,
|
||||||
|
entitlement: {
|
||||||
|
id: entitlement.id,
|
||||||
|
type: entitlement.entitlement_type,
|
||||||
|
scope: entitlement.scope,
|
||||||
|
data: entitlement.data,
|
||||||
|
issuer: {
|
||||||
|
id: entitlement.issuer?.id,
|
||||||
|
name: entitlement.issuer?.name,
|
||||||
|
class: entitlement.issuer?.issuer_class,
|
||||||
|
},
|
||||||
|
issued_at: entitlement.created_at,
|
||||||
|
expires_at: entitlement.expires_at,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Verify error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to verify entitlement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/os/entitlements/resolve", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { platform, id, subject_id } = req.query;
|
||||||
|
|
||||||
|
let entitlements: any[] = [];
|
||||||
|
|
||||||
|
if (subject_id) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.select("*, issuer:aethex_issuers(*)")
|
||||||
|
.eq("subject_id", subject_id as string)
|
||||||
|
.eq("status", "active");
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
entitlements = data || [];
|
||||||
|
} else if (platform && id) {
|
||||||
|
const externalRef = `${platform}:${id}`;
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.select("*, issuer:aethex_issuers(*)")
|
||||||
|
.eq("external_subject_ref", externalRef)
|
||||||
|
.eq("status", "active");
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
entitlements = data || [];
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: "Missing platform/id or subject_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
entitlements: entitlements.map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
type: e.entitlement_type,
|
||||||
|
scope: e.scope,
|
||||||
|
data: e.data,
|
||||||
|
issuer: {
|
||||||
|
name: e.issuer?.name,
|
||||||
|
class: e.issuer?.issuer_class,
|
||||||
|
},
|
||||||
|
issued_at: e.created_at,
|
||||||
|
expires_at: e.expires_at,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Resolve error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to resolve entitlements" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/os/entitlements/revoke", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const issuerId = req.headers["x-issuer-id"] as string;
|
||||||
|
const { entitlement_id, reason } = req.body;
|
||||||
|
|
||||||
|
if (!entitlement_id || !reason) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Missing entitlement_id or reason" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("aethex_entitlements")
|
||||||
|
.update({
|
||||||
|
status: "revoked",
|
||||||
|
revoked_at: new Date().toISOString(),
|
||||||
|
revocation_reason: reason,
|
||||||
|
})
|
||||||
|
.eq("id", entitlement_id)
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await supabase.from("aethex_entitlement_events").insert({
|
||||||
|
entitlement_id,
|
||||||
|
event_type: "revoked",
|
||||||
|
actor_id: issuerId,
|
||||||
|
actor_type: "issuer",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
await supabase.from("aethex_audit_log").insert({
|
||||||
|
action: "revoke_entitlement",
|
||||||
|
actor_id: issuerId,
|
||||||
|
actor_type: "issuer",
|
||||||
|
resource_type: "entitlement",
|
||||||
|
resource_id: entitlement_id,
|
||||||
|
changes: { status: "revoked", reason },
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: "Entitlement revoked" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Revoke error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to revoke entitlement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/os/issuers/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const { data: issuer, error } = await supabase
|
||||||
|
.from("aethex_issuers")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !issuer) {
|
||||||
|
return res.status(404).json({ error: "Issuer not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: issuer.id,
|
||||||
|
name: issuer.name,
|
||||||
|
class: issuer.issuer_class,
|
||||||
|
scopes: issuer.scopes,
|
||||||
|
public_key: issuer.public_key,
|
||||||
|
is_active: issuer.is_active,
|
||||||
|
metadata: issuer.metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Issuer fetch error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch issuer" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { pgTable, text, varchar, boolean, integer, timestamp, json, decimal } from "drizzle-orm/pg-core";
|
import { pgTable, text, varchar, boolean, integer, timestamp, json, decimal } from "drizzle-orm/pg-core";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
@ -620,3 +621,98 @@ export const insertCustomAppSchema = createInsertSchema(custom_apps).omit({
|
||||||
|
|
||||||
export type InsertCustomApp = z.infer<typeof insertCustomAppSchema>;
|
export type InsertCustomApp = z.infer<typeof insertCustomAppSchema>;
|
||||||
export type CustomApp = typeof custom_apps.$inferSelect;
|
export type CustomApp = typeof custom_apps.$inferSelect;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// OS KERNEL SCHEMA (Portable Proof System)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Subjects: Internal coordination IDs
|
||||||
|
export const aethex_subjects = pgTable("aethex_subjects", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subject Identities: External ID bindings (Roblox, Discord, GitHub, Epic)
|
||||||
|
export const aethex_subject_identities = pgTable("aethex_subject_identities", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
subject_id: varchar("subject_id").notNull(),
|
||||||
|
provider: varchar("provider").notNull(), // "roblox" | "discord" | "github" | "epic"
|
||||||
|
external_id: varchar("external_id").notNull(),
|
||||||
|
external_username: varchar("external_username"),
|
||||||
|
verified_at: timestamp("verified_at"),
|
||||||
|
revoked_at: timestamp("revoked_at"),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Issuers: Who can issue entitlements
|
||||||
|
export const aethex_issuers = pgTable("aethex_issuers", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
issuer_class: varchar("issuer_class").notNull(), // "lab" | "platform" | "foundation" | "external"
|
||||||
|
scopes: json("scopes").$type<string[]>().default(sql`'[]'::json`),
|
||||||
|
public_key: text("public_key").notNull(),
|
||||||
|
is_active: boolean("is_active").default(true),
|
||||||
|
metadata: json("metadata").$type<Record<string, any>>().default(sql`'{}'::json`),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
updated_at: timestamp("updated_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Issuer Keys: Key rotation
|
||||||
|
export const aethex_issuer_keys = pgTable("aethex_issuer_keys", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
issuer_id: varchar("issuer_id").notNull(),
|
||||||
|
public_key: text("public_key").notNull(),
|
||||||
|
private_key_hash: text("private_key_hash"),
|
||||||
|
is_active: boolean("is_active").default(true),
|
||||||
|
rotated_at: timestamp("rotated_at"),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entitlements: The proofs
|
||||||
|
export const aethex_entitlements = pgTable("aethex_entitlements", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
issuer_id: varchar("issuer_id").notNull(),
|
||||||
|
subject_id: varchar("subject_id"),
|
||||||
|
external_subject_ref: varchar("external_subject_ref"), // "roblox:12345"
|
||||||
|
schema_version: varchar("schema_version").default("v0.1"),
|
||||||
|
scope: varchar("scope").notNull(), // "achievement" | "project" | "release"
|
||||||
|
entitlement_type: varchar("entitlement_type").notNull(),
|
||||||
|
data: json("data").$type<Record<string, any>>().notNull(),
|
||||||
|
status: varchar("status").default("active"), // "active" | "revoked" | "expired"
|
||||||
|
signature: text("signature"),
|
||||||
|
evidence_hash: varchar("evidence_hash"),
|
||||||
|
issued_by_subject_id: varchar("issued_by_subject_id"),
|
||||||
|
expires_at: timestamp("expires_at"),
|
||||||
|
revoked_at: timestamp("revoked_at"),
|
||||||
|
revocation_reason: text("revocation_reason"),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
updated_at: timestamp("updated_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entitlement Events: Audit trail
|
||||||
|
export const aethex_entitlement_events = pgTable("aethex_entitlement_events", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
entitlement_id: varchar("entitlement_id").notNull(),
|
||||||
|
event_type: varchar("event_type").notNull(), // "issued" | "verified" | "revoked" | "expired"
|
||||||
|
actor_id: varchar("actor_id"),
|
||||||
|
actor_type: varchar("actor_type").notNull(), // "user" | "issuer" | "system"
|
||||||
|
reason: text("reason"),
|
||||||
|
metadata: json("metadata").$type<Record<string, any>>().default(sql`'{}'::json`),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Audit Log: All OS actions
|
||||||
|
export const aethex_audit_log = pgTable("aethex_audit_log", {
|
||||||
|
id: varchar("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
action: varchar("action").notNull(), // "link_identity" | "issue_entitlement" | etc
|
||||||
|
actor_id: varchar("actor_id"),
|
||||||
|
actor_type: varchar("actor_type").notNull(), // "user" | "issuer" | "admin" | "system"
|
||||||
|
resource_type: varchar("resource_type").notNull(), // "subject" | "entitlement" | "issuer"
|
||||||
|
resource_id: varchar("resource_id").notNull(),
|
||||||
|
changes: json("changes").$type<Record<string, any>>().default(sql`'{}'::json`),
|
||||||
|
ip_address: varchar("ip_address"),
|
||||||
|
user_agent: text("user_agent"),
|
||||||
|
status: varchar("status").default("success"), // "success" | "failure"
|
||||||
|
error_message: text("error_message"),
|
||||||
|
created_at: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue