diff --git a/client/src/pages/terminal.tsx b/client/src/pages/terminal.tsx index 432c2e0..aee1394 100644 --- a/client/src/pages/terminal.tsx +++ b/client/src/pages/terminal.tsx @@ -6,6 +6,7 @@ import { ArrowLeft, Terminal as TerminalIcon, Copy, Trash2 } from "lucide-react" import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; +import { useToast } from "@/hooks/use-toast"; interface TerminalLine { type: 'input' | 'output' | 'error' | 'system'; @@ -27,6 +28,7 @@ export default function Terminal() { const [cliLabel, setCliLabel] = useState(""); const eventSourceRef = useRef(null); const currentRunId = useRef(null); + const { toast } = useToast(); useEffect(() => { return () => { @@ -188,6 +190,7 @@ export default function Terminal() { if (cliStatus === "running") return; setCliStatus("running"); appendCliLine('system', `▸ Running ${cliCommand}...`); + toast({ title: "CLI", description: `Started ${cliCommand}`, variant: "default" }); try { const res = await fetch('/api/admin/cli/start', { @@ -201,6 +204,7 @@ export default function Terminal() { const text = await res.text(); appendCliLine('error', `Start failed: ${text || res.status}`); setCliStatus("error"); + toast({ title: "CLI Error", description: `Failed to start ${cliCommand}`, variant: "destructive" }); return; } @@ -218,6 +222,7 @@ export default function Terminal() { es.addEventListener('error', (evt) => { appendCliLine('error', 'Stream error'); setCliStatus("error"); + toast({ title: "CLI Error", description: `Stream error for ${cliCommand}`, variant: "destructive" }); es.close(); }); @@ -225,6 +230,7 @@ export default function Terminal() { const status = evt.data === 'success' ? 'done' : 'error'; setCliStatus(status as any); appendCliLine(status === 'done' ? 'system' : 'error', `▸ ${cliLabel || cliCommand} ${status}`); + toast({ title: status === 'done' ? "CLI Success" : "CLI Failed", description: `${cliLabel || cliCommand} ${status}` }); es.close(); currentRunId.current = null; }); @@ -232,6 +238,7 @@ export default function Terminal() { } catch (err) { appendCliLine('error', 'Failed to start CLI command'); setCliStatus("error"); + toast({ title: "CLI Error", description: `Failed to start ${cliCommand}`, variant: "destructive" }); } }; diff --git a/docs/ENTITLEMENTS_QUICKSTART.md b/docs/ENTITLEMENTS_QUICKSTART.md new file mode 100644 index 0000000..6efa24a --- /dev/null +++ b/docs/ENTITLEMENTS_QUICKSTART.md @@ -0,0 +1,141 @@ +# Entitlements Quickstart + +This guide helps you set up the OS Kernel (identity + entitlements) and use the API to issue, verify, resolve, and revoke entitlements. + +## 1) Run OS Kernel Migration + +Ensure `DATABASE_URL` is set in your environment (Railway/Supabase Postgres). Then run the OS Kernel migration. You can do this either from the dev shell or the in-app Terminal (Admin only). + +- Dev shell: + - `npx ts-node script/run-os-migration.ts` +- In-app Terminal: + - Command: `migrate-os` (now available in the command dropdown) + +This creates tables: +- `aethex_subjects`, `aethex_subject_identities` +- `aethex_issuers`, `aethex_issuer_keys` +- `aethex_entitlements`, `aethex_entitlement_events` +- `aethex_audit_log` + +## 2) Create an Issuer + +Insert an issuer record with a `public_key` and optional metadata. You can use SQL or your DB console (Supabase) for now. + +Example SQL: + +```sql +INSERT INTO public.aethex_issuers (name, issuer_class, scopes, public_key, is_active) +VALUES ('AeThex Platform', 'platform', '["issue","revoke"]'::json, 'PUBLIC_KEY_STRING', true) +RETURNING id; +``` + +Save the returned `id` as your `issuer_id`. + +## 3) Issue an Entitlement + +Endpoint: `POST /api/os/entitlements/issue` + +Headers: +- `x-issuer-id: ` + +Body: +```json +{ + "subject_id": "", + "external_subject_ref": "roblox:12345", + "entitlement_type": "achievement", + "scope": "project", + "data": { "name": "Alpha Access" }, + "expires_at": null +} +``` + +Response: +```json +{ + "success": true, + "entitlement": { + "id": "...", + "type": "achievement", + "scope": "project", + "created_at": "..." + } +} +``` + +## 4) Verify an Entitlement + +Endpoint: `POST /api/os/entitlements/verify` + +Body: +```json +{ "entitlement_id": "..." } +``` + +Response (valid): +```json +{ + "valid": true, + "entitlement": { + "id": "...", + "type": "achievement", + "scope": "project", + "data": { "name": "Alpha Access" }, + "issuer": { "id": "...", "name": "AeThex Platform", "class": "platform" }, + "issued_at": "...", + "expires_at": null + } +} +``` + +Response (revoked/expired): +```json +{ "valid": false, "reason": "revoked|expired", ... } +``` + +## 5) Resolve Entitlements (by subject or external ref) + +Endpoint: `GET /api/os/entitlements/resolve` + +Query params (choose one path): +- `?subject_id=` +- `?platform=roblox&id=12345` + +Response: +```json +{ + "entitlements": [ + { + "id": "...", + "type": "achievement", + "scope": "project", + "data": { "name": "Alpha Access" }, + "issuer": { "name": "AeThex Platform", "class": "platform" }, + "issued_at": "...", + "expires_at": null + } + ] +} +``` + +## 6) Revoke an Entitlement + +Endpoint: `POST /api/os/entitlements/revoke` + +Headers: +- `x-issuer-id: ` + +Body: +```json +{ "entitlement_id": "...", "reason": "Fraudulent use" } +``` + +Response: +```json +{ "success": true, "message": "Entitlement revoked" } +``` + +## Notes +- All OS routes are protected by the capability guard and expect authenticated context where relevant. +- Use Supabase console to inspect tables and audit logs. +- For production, plan issuer key rotation via `aethex_issuer_keys`; rotation endpoints can be added similarly. diff --git a/script/seed.ts b/script/seed.ts new file mode 100644 index 0000000..ac69329 --- /dev/null +++ b/script/seed.ts @@ -0,0 +1,39 @@ +import dotenv from "dotenv"; +import pkg from "pg"; + +dotenv.config(); +const { Client } = pkg as any; + +async function main() { + const client = new Client({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false } }); + await client.connect(); + + try { + console.log("Seeding default issuer if missing..."); + const name = "AeThex Platform"; + + const existing = await client.query( + 'SELECT id FROM public.aethex_issuers WHERE name = $1 LIMIT 1;', + [name] + ); + + if (existing.rows.length) { + console.log(`Issuer exists: ${existing.rows[0].id}`); + } else { + const insert = await client.query( + 'INSERT INTO public.aethex_issuers (name, issuer_class, scopes, public_key, is_active) VALUES ($1, $2, $3::json, $4, $5) RETURNING id;', + [name, 'platform', JSON.stringify(["issue","revoke"]), 'PUBLIC_KEY_STRING', true] + ); + console.log(`Issuer created: ${insert.rows[0].id}`); + } + + console.log("Done."); + } catch (err: any) { + console.error("Seed failed:", err.message || err); + process.exit(1); + } finally { + await client.end(); + } +} + +main(); diff --git a/server/routes.ts b/server/routes.ts index 578a3ef..542be97 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -47,6 +47,7 @@ export async function registerRoutes( build: { cmd: "npm", args: ["run", "build"], label: "npm run build" }, "migrate-status": { cmd: "npx", args: ["drizzle-kit", "status"], label: "drizzle status" }, migrate: { cmd: "npx", args: ["drizzle-kit", "migrate:push"], label: "drizzle migrate" }, + "migrate-os": { cmd: "npx", args: ["ts-node", "script/run-os-migration.ts"], label: "os kernel migrate" }, seed: { cmd: "npx", args: ["ts-node", "script/seed.ts"], label: "seed" }, test: { cmd: "bash", args: ["./test-implementation.sh"], label: "test-implementation" }, };