From 929d293e5f3cf3021f08e7355d98ea2571684195 Mon Sep 17 00:00:00 2001 From: MrPiglr <31398225+MrPiglr@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:25:27 +0000 Subject: [PATCH] new file: server/api/os.ts --- client/src/App.tsx | 34 +- client/src/pages/{ => hub}/analytics.tsx | 0 client/src/pages/{ => hub}/code-gallery.tsx | 0 client/src/pages/{ => hub}/file-manager.tsx | 0 client/src/pages/{ => hub}/marketplace.tsx | 0 client/src/pages/{ => hub}/messaging.tsx | 0 client/src/pages/{ => hub}/notifications.tsx | 0 client/src/pages/{ => hub}/projects.tsx | 0 client/src/pages/{ => hub}/settings.tsx | 0 client/src/pages/os/link.tsx | 216 ++++++++++ migrations/0001_new_apps_expansion.sql | 19 +- migrations/0002_os_kernel.sql | 111 +++++ script/check-tables.ts | 54 +++ script/migrate-os.ts | 53 +++ script/run-os-migration.ts | 56 +++ server/api/os.ts | 406 +++++++++++++++++++ server/routes.ts | 368 +++++++++++++++++ shared/schema.ts | 96 +++++ 18 files changed, 1387 insertions(+), 26 deletions(-) rename client/src/pages/{ => hub}/analytics.tsx (100%) rename client/src/pages/{ => hub}/code-gallery.tsx (100%) rename client/src/pages/{ => hub}/file-manager.tsx (100%) rename client/src/pages/{ => hub}/marketplace.tsx (100%) rename client/src/pages/{ => hub}/messaging.tsx (100%) rename client/src/pages/{ => hub}/notifications.tsx (100%) rename client/src/pages/{ => hub}/projects.tsx (100%) rename client/src/pages/{ => hub}/settings.tsx (100%) create mode 100644 client/src/pages/os/link.tsx create mode 100644 migrations/0002_os_kernel.sql create mode 100644 script/check-tables.ts create mode 100644 script/migrate-os.ts create mode 100644 script/run-os-migration.ts create mode 100644 server/api/os.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index e3fc11a..1612a15 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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() { {() => } + {() => } - {() => } - {() => } - {() => } - {() => } - {() => } - {() => } - {() => } - {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } ); diff --git a/client/src/pages/analytics.tsx b/client/src/pages/hub/analytics.tsx similarity index 100% rename from client/src/pages/analytics.tsx rename to client/src/pages/hub/analytics.tsx diff --git a/client/src/pages/code-gallery.tsx b/client/src/pages/hub/code-gallery.tsx similarity index 100% rename from client/src/pages/code-gallery.tsx rename to client/src/pages/hub/code-gallery.tsx diff --git a/client/src/pages/file-manager.tsx b/client/src/pages/hub/file-manager.tsx similarity index 100% rename from client/src/pages/file-manager.tsx rename to client/src/pages/hub/file-manager.tsx diff --git a/client/src/pages/marketplace.tsx b/client/src/pages/hub/marketplace.tsx similarity index 100% rename from client/src/pages/marketplace.tsx rename to client/src/pages/hub/marketplace.tsx diff --git a/client/src/pages/messaging.tsx b/client/src/pages/hub/messaging.tsx similarity index 100% rename from client/src/pages/messaging.tsx rename to client/src/pages/hub/messaging.tsx diff --git a/client/src/pages/notifications.tsx b/client/src/pages/hub/notifications.tsx similarity index 100% rename from client/src/pages/notifications.tsx rename to client/src/pages/hub/notifications.tsx diff --git a/client/src/pages/projects.tsx b/client/src/pages/hub/projects.tsx similarity index 100% rename from client/src/pages/projects.tsx rename to client/src/pages/hub/projects.tsx diff --git a/client/src/pages/settings.tsx b/client/src/pages/hub/settings.tsx similarity index 100% rename from client/src/pages/settings.tsx rename to client/src/pages/hub/settings.tsx diff --git a/client/src/pages/os/link.tsx b/client/src/pages/os/link.tsx new file mode 100644 index 0000000..204c79c --- /dev/null +++ b/client/src/pages/os/link.tsx @@ -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 ( +
+
+ {/* Header */} +
+

Identity Linking

+

+ Link your accounts to get verified credentials across the AeThex ecosystem. +

+

+ šŸ’” What this means: Your proofs are portable. Link once, use everywhere. +

+
+ + {/* Providers */} +
+

Available Platforms

+ {providers.map((provider) => { + const Icon = provider.icon; + const isLinked = linkedIdentities.some((id) => id.provider === provider.id); + + return ( + + +
+ +
+

{provider.name}

+ {isLinked && ( +

+ āœ“ Linked and verified +

+ )} +
+
+ +
+
+ ); + })} +
+ + {/* Info Cards */} +
+ + + + šŸ” Your Privacy + + + + We never share your linked identities without your consent. Each platform only sees what you allow. + + + + + + + āœ“ Verified Proofs + + + + When you link, we create cryptographically signed proofs of your achievements that you can share. + + + + + + + šŸ”— Portable + + + + Use your verified credentials across any platform that trusts AeThex, without creating new accounts. + + + + + + + 🚪 Exit Path + + + + If AeThex disappears, your proofs remain valid and your linked accounts are still yours. + + +
+ + {/* How It Works */} + + + How It Works + + +
+
+
1
+
+

Link Your Account

+

Connect your Roblox, Discord, or GitHub account securely.

+
+
+
+
2
+
+

Verify Ownership

+

We confirm you own the account (OAuth, challenge, etc).

+
+
+
+
3
+
+

Get Verified Proofs

+

Your achievements are signed and portable across platforms.

+
+
+
+
4
+
+

Use Everywhere

+

Share your proofs with any platform that trusts AeThex OS.

+
+
+
+
+
+ + {/* Footer */} +
+

+ šŸ’” OS attests; platforms decide. +

+

We verify your credentials. Other platforms decide what access to grant.

+
+
+
+ ); +} diff --git a/migrations/0001_new_apps_expansion.sql b/migrations/0001_new_apps_expansion.sql index 9b9b80a..debfe1a 100644 --- a/migrations/0001_new_apps_expansion.sql +++ b/migrations/0001_new_apps_expansion.sql @@ -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) + diff --git a/migrations/0002_os_kernel.sql b/migrations/0002_os_kernel.sql new file mode 100644 index 0000000..84cbc7f --- /dev/null +++ b/migrations/0002_os_kernel.sql @@ -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 diff --git a/script/check-tables.ts b/script/check-tables.ts new file mode 100644 index 0000000..cadb5b0 --- /dev/null +++ b/script/check-tables.ts @@ -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(); diff --git a/script/migrate-os.ts b/script/migrate-os.ts new file mode 100644 index 0000000..4d424f5 --- /dev/null +++ b/script/migrate-os.ts @@ -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(); diff --git a/script/run-os-migration.ts b/script/run-os-migration.ts new file mode 100644 index 0000000..e4f4e41 --- /dev/null +++ b/script/run-os-migration.ts @@ -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(); diff --git a/server/api/os.ts b/server/api/os.ts new file mode 100644 index 0000000..570d56c --- /dev/null +++ b/server/api/os.ts @@ -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; diff --git a/server/routes.ts b/server/routes.ts index 2ad3e4d..d010dbd 100644 --- a/server/routes.ts +++ b/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; } diff --git a/shared/schema.ts b/shared/schema.ts index 1516f7a..09523eb 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -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; 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().default(sql`'[]'::json`), + public_key: text("public_key").notNull(), + is_active: boolean("is_active").default(true), + metadata: json("metadata").$type>().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>().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>().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>().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(), +});