mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +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 NetworkProfile from "@/pages/network-profile";
|
||||
import Lab from "@/pages/lab";
|
||||
import Projects from "@/pages/projects";
|
||||
import Messaging from "@/pages/messaging";
|
||||
import Marketplace from "@/pages/marketplace";
|
||||
import Settings from "@/pages/settings";
|
||||
import FileManager from "@/pages/file-manager";
|
||||
import CodeGallery from "@/pages/code-gallery";
|
||||
import Notifications from "@/pages/notifications";
|
||||
import Analytics from "@/pages/analytics";
|
||||
import HubProjects from "@/pages/hub/projects";
|
||||
import HubMessaging from "@/pages/hub/messaging";
|
||||
import HubMarketplace from "@/pages/hub/marketplace";
|
||||
import HubSettings from "@/pages/hub/settings";
|
||||
import HubFileManager from "@/pages/hub/file-manager";
|
||||
import HubCodeGallery from "@/pages/hub/code-gallery";
|
||||
import HubNotifications from "@/pages/hub/notifications";
|
||||
import HubAnalytics from "@/pages/hub/analytics";
|
||||
import OsLink from "@/pages/os/link";
|
||||
import { LabTerminalProvider } from "@/hooks/use-lab-terminal";
|
||||
|
||||
function Router() {
|
||||
|
|
@ -67,17 +68,18 @@ function Router() {
|
|||
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
|
||||
<Route path="/pitch" component={Pitch} />
|
||||
<Route path="/os" component={AeThexOS} />
|
||||
<Route path="/os/link">{() => <ProtectedRoute><OsLink /></ProtectedRoute>}</Route>
|
||||
<Route path="/network" component={Network} />
|
||||
<Route path="/network/:slug" component={NetworkProfile} />
|
||||
<Route path="/lab" component={Lab} />
|
||||
<Route path="/projects">{() => <ProtectedRoute><Projects /></ProtectedRoute>}</Route>
|
||||
<Route path="/messaging">{() => <ProtectedRoute><Messaging /></ProtectedRoute>}</Route>
|
||||
<Route path="/marketplace">{() => <ProtectedRoute><Marketplace /></ProtectedRoute>}</Route>
|
||||
<Route path="/settings">{() => <ProtectedRoute><Settings /></ProtectedRoute>}</Route>
|
||||
<Route path="/file-manager">{() => <ProtectedRoute><FileManager /></ProtectedRoute>}</Route>
|
||||
<Route path="/code-gallery">{() => <ProtectedRoute><CodeGallery /></ProtectedRoute>}</Route>
|
||||
<Route path="/notifications">{() => <ProtectedRoute><Notifications /></ProtectedRoute>}</Route>
|
||||
<Route path="/analytics">{() => <ProtectedRoute><Analytics /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/projects">{() => <ProtectedRoute><HubProjects /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/messaging">{() => <ProtectedRoute><HubMessaging /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/marketplace">{() => <ProtectedRoute><HubMarketplace /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/settings">{() => <ProtectedRoute><HubSettings /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/file-manager">{() => <ProtectedRoute><HubFileManager /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/code-gallery">{() => <ProtectedRoute><HubCodeGallery /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/notifications">{() => <ProtectedRoute><HubNotifications /></ProtectedRoute>}</Route>
|
||||
<Route path="/hub/analytics">{() => <ProtectedRoute><HubAnalytics /></ProtectedRoute>}</Route>
|
||||
<Route component={NotFound} />
|
||||
</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 INDEX IF NOT EXISTS "messages_sender_id_idx" ON "messages" ("sender_id");
|
||||
--> 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
|
||||
CREATE INDEX IF NOT EXISTS "marketplace_listings_seller_id_idx" ON "marketplace_listings" ("seller_id");
|
||||
-- Removed: marketplace_listings_category_idx (schema mismatch)
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "marketplace_listings_category_idx" ON "marketplace_listings" ("category");
|
||||
-- Removed: files_user_id_idx (schema mismatch)
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "files_user_id_idx" ON "files" ("user_id");
|
||||
-- Removed: files_parent_id_idx (schema mismatch)
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "files_parent_id_idx" ON "files" ("parent_id");
|
||||
-- Removed: notifications_user_id_idx (schema mismatch)
|
||||
--> 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
|
||||
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
|
||||
CREATE INDEX IF NOT EXISTS "code_gallery_creator_id_idx" ON "code_gallery" ("creator_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "projects_user_id_idx" ON "projects" ("user_id");
|
||||
-- Removed: projects_user_id_idx (schema mismatch)
|
||||
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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 { z } from "zod";
|
||||
|
||||
|
|
@ -620,3 +621,98 @@ export const insertCustomAppSchema = createInsertSchema(custom_apps).omit({
|
|||
|
||||
export type InsertCustomApp = z.infer<typeof insertCustomAppSchema>;
|
||||
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