/** * system-access.ts — Device authorization flow for system MCP tools * * Flow: Claude calls system_request_access → backend notifies browser via WS → * user approves in /dev → Claude polls system_check_access → gets systemToken */ import { randomUUID } from 'crypto'; import { pushEvent } from './mcp/events.ts'; const REQUEST_TTL_MS = 5 * 60 * 1000; // 5 min to approve const TOKEN_TTL_MS = 4 * 60 * 60 * 1000; // 4 hour token lifetime export interface PendingRequest { requestId: string; userCode: string; description: string; createdAt: number; expiresAt: number; } interface RequestEntry extends PendingRequest { systemToken: string | null; mcpKey?: string; } interface SystemTokenEntry { requestId: string; user: string; createdAt: number; expiresAt: number; } const pendingMap = new Map(); const tokenMap = new Map(); let notifyFn: ((req: PendingRequest) => void) | null = null; export function setNotifyFn(fn: (req: PendingRequest) => void) { notifyFn = fn; } function genUserCode(): string { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const part = (n: number) => Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); return `${part(4)}-${part(4)}`; } export function createRequest(description: string, mcpKey?: string): PendingRequest { const now = Date.now(); // Clean expired for (const [id, req] of pendingMap) if (req.expiresAt < now) pendingMap.delete(id); const entry: RequestEntry = { requestId: randomUUID(), userCode: genUserCode(), description, createdAt: now, expiresAt: now + REQUEST_TTL_MS, systemToken: null, mcpKey, }; pendingMap.set(entry.requestId, entry); const pub: PendingRequest = { requestId: entry.requestId, userCode: entry.userCode, description, createdAt: entry.createdAt, expiresAt: entry.expiresAt }; notifyFn?.(pub); return pub; } export function getPendingRequests(): PendingRequest[] { const now = Date.now(); const out: PendingRequest[] = []; for (const req of pendingMap.values()) if (req.expiresAt > now && !req.systemToken) out.push({ requestId: req.requestId, userCode: req.userCode, description: req.description, createdAt: req.createdAt, expiresAt: req.expiresAt }); return out; } export function approveRequest(requestId: string, user: string): string | null { const req = pendingMap.get(requestId); if (!req) return null; if (req.expiresAt < Date.now()) { pendingMap.delete(requestId); return null; } if (req.systemToken) return req.systemToken; // idempotent const token = randomUUID(); // Push event to MCP subscriber if (req.mcpKey) pushEvent(req.mcpKey, { type: "system_access_approved", data: { requestId, systemToken: token } }); req.systemToken = token; tokenMap.set(token, { requestId, user, createdAt: Date.now(), expiresAt: Date.now() + TOKEN_TTL_MS }); return token; } export function denyRequest(requestId: string) { pendingMap.delete(requestId); } export function checkRequest(requestId: string): { status: 'pending' | 'approved' | 'expired' | 'denied'; systemToken?: string } { const req = pendingMap.get(requestId); if (!req) return { status: 'denied' }; if (req.expiresAt < Date.now()) { pendingMap.delete(requestId); return { status: 'expired' }; } if (req.systemToken) return { status: 'approved', systemToken: req.systemToken }; return { status: 'pending' }; } export function validateSystemToken(token: string): SystemTokenEntry | null { const entry = tokenMap.get(token); if (!entry) return null; if (entry.expiresAt < Date.now()) { tokenMap.delete(token); return null; } return entry; }