import { type AccessScope } from "@/lib/accessTokens/scopes"; /** * Required-scope inference for remote CLI access tokens. * * Policy (owner-confirmed 2026-06-19): infer from HTTP method — * - GET/HEAD/OPTIONS → read * - POST/PUT/PATCH/DELETE → write * with two admin overrides: * - ADMIN_SCOPE_PREFIXES: admin for ANY method (inherently sensitive surfaces). * - ADMIN_MUTATION_PREFIXES: admin only when mutating; GET/HEAD stay read so a * `read` token can still inspect status under these prefixes. * * This covers every existing management route with zero per-route edits. A NEW * mutating route nasce exigindo `write` por padrão (não `admin`) — quando uma * rota nova for sensível, adicione seu prefixo a uma das listas abaixo. * * Note: this only governs the access-token credential path. Dashboard JWT, the * loopback CLI machine-id token, and manage-scope API keys remain full-access. * Loopback-only routes that spawn processes are blocked before auth regardless. */ /** Sensitive management surfaces — require `admin` for ALL methods. */ export const ADMIN_SCOPE_PREFIXES: readonly string[] = [ "/api/cli/tokens", // access-token management (create/list/revoke) "/api/oauth", // OAuth authorization flows "/api/auth", // login / logout / session "/api/policy", // policy engine "/api/services", // embedded-service lifecycle (also loopback-blocked) "/api/mcp", // MCP process surface (also loopback-blocked) ]; /** Require `admin` only for mutating methods; GET/HEAD under these stay `read`. */ export const ADMIN_MUTATION_PREFIXES: readonly string[] = [ "/api/providers", // POST add provider / rotate key = admin; GET status = read "/api/cli-tools/apply", // writes config onto the host filesystem ]; const READ_METHODS = new Set(["GET", "HEAD", "OPTIONS"]); function matchesPrefix(path: string, prefixes: readonly string[]): boolean { // Exact match or a true path-segment boundary (`pre/...`). A bare // `startsWith(pre)` would over-match lookalikes (e.g. "/api/auth" vs // "/api/authz-inventory"), so it is intentionally NOT used. return prefixes.some((pre) => path === pre || path.startsWith(pre + "/")); } /** * Determine the minimum access-token scope required to call `method path`. * Pure + deterministic — safe to unit test exhaustively. */ export function inferRequiredScope(method: string, path: string): AccessScope { const m = (method || "GET").toUpperCase(); const p = path || "/"; if (matchesPrefix(p, ADMIN_SCOPE_PREFIXES)) return "admin"; const isMutation = !READ_METHODS.has(m); if (isMutation && matchesPrefix(p, ADMIN_MUTATION_PREFIXES)) return "admin"; return isMutation ? "write" : "read"; }