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
+
+ )}
+
+
+
+ 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 ? (
+ <>
+
+ Linking...
+ >
+ ) : isLinked ? (
+ "Unlink"
+ ) : (
+ "Link"
+ )}
+
+
+
+ );
+ })}
+
+
+ {/* 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(),
+});