import { NextResponse } from "next/server"; import { getSettings } from "@/lib/localDb"; import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; import { createErrorResponse } from "@/lib/api/errorResponse"; import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; import { LOCAL_ONLY_API_PREFIXES, ALWAYS_PROTECTED_API_PATHS, LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES, SPAWN_CAPABLE_PREFIXES, } from "@/server/authz/routeGuard"; /** * Static MANAGEMENT-tier example prefixes. Render-only — never consulted by * the runtime policy. The actual MANAGEMENT classification is "any /api/* * that is not LOCAL_ONLY, not v1/client, not on the public allowlist", so the * inventory shows representative entries rather than a generated enumeration. */ const MANAGEMENT_TIER_PREFIXES: ReadonlyArray = [ "/api/settings", "/api/providers/", "/api/api-keys", ]; const CLIENT_API_TIER_PREFIXES: ReadonlyArray = ["/v1/", "/api/v1/"]; const PUBLIC_TIER_PREFIXES: ReadonlyArray = ["/api/health", "/api/version", "/_next/"]; type TierName = "LOCAL_ONLY" | "ALWAYS_PROTECTED" | "MANAGEMENT" | "CLIENT_API" | "PUBLIC"; interface TierEntry { name: TierName; prefixes: string[]; description: string; bypassable: boolean; } /** * OQ-5: viewing the inventory requires authentication (dashboard session OR * any valid API key, regardless of scope). The inventory leaks route-prefix * taxonomy + current bypass state (reconnaissance value), so we never expose * it anonymously — but a non-manage key holder may still inspect it. * * Compare with `requireManagementAuth` which would refuse anything below the * manage scope; this endpoint is intentionally read-only and one rung lower. */ async function requireInventoryReadAuth(request: Request): Promise { if (!(await isAuthRequired(request))) { return null; } if (await isDashboardSessionAuthenticated(request)) { return null; } const apiKey = extractApiKey(request); if (apiKey) { try { if (await isValidApiKey(apiKey)) { return null; } } catch { return createErrorResponse({ status: 503, message: "Service temporarily unavailable", type: "server_error", }); } return createErrorResponse({ status: 403, message: "Invalid API key", type: "invalid_request", }); } return createErrorResponse({ status: 401, message: "Authentication required", type: "invalid_request", }); } function isBypassableConstant(prefix: string): boolean { // A LOCAL_ONLY prefix is bypassable iff it appears in the compile-time // bypass constant AND is not a SPAWN_CAPABLE prefix. Runtime DB state is // surfaced separately via `bypassEnabled` / `bypassPrefixes`. if (SPAWN_CAPABLE_PREFIXES.some((p) => p === prefix || prefix.startsWith(p))) { return false; } return LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES.some((p) => p === prefix); } export async function GET(request: Request) { const authError = await requireInventoryReadAuth(request); if (authError) return authError; try { const settings = await getSettings(); const tiers: TierEntry[] = [ { name: "LOCAL_ONLY", prefixes: [...LOCAL_ONLY_API_PREFIXES], description: "Loopback-only routes. Spawn child processes; exposing them to non-local traffic is a known CVE class (GHSA-fhh6-4qxv-rpqj). Some entries are opt-in bypassable via the manage scope (kill-switch gated).", bypassable: LOCAL_ONLY_API_PREFIXES.some(isBypassableConstant), }, { name: "ALWAYS_PROTECTED", prefixes: [...ALWAYS_PROTECTED_API_PATHS], description: "Auth required unconditionally, even when requireLogin=false. Covers destructive / irreversible operations (shutdown, database settings).", bypassable: false, }, { name: "MANAGEMENT", prefixes: [...MANAGEMENT_TIER_PREFIXES], description: "Default tier for /api/* admin endpoints. Auth required unless requireLogin=false. PATCHes touching security-impacting keys require currentPassword re-auth.", bypassable: false, }, { name: "CLIENT_API", prefixes: [...CLIENT_API_TIER_PREFIXES], description: "Client-facing inference APIs. Accept Bearer API keys; not gated by dashboard sessions.", bypassable: false, }, { name: "PUBLIC", prefixes: [...PUBLIC_TIER_PREFIXES], description: "Unauthenticated routes: health probes, public assets, onboarding bootstrap.", bypassable: false, }, ]; const bypassEnabled = typeof settings.localOnlyManageScopeBypassEnabled === "boolean" ? settings.localOnlyManageScopeBypassEnabled : true; const bypassPrefixesRaw = settings.localOnlyManageScopeBypassPrefixes; const bypassPrefixes = Array.isArray(bypassPrefixesRaw) ? bypassPrefixesRaw.filter((p): p is string => typeof p === "string") : [...LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES]; return NextResponse.json({ tiers, bypassEnabled, bypassPrefixes, spawnCapablePrefixes: [...SPAWN_CAPABLE_PREFIXES], }); } catch (error) { console.log("Error loading authz inventory:", error); return NextResponse.json({ error: "Failed to load authz inventory" }, { status: 500 }); } }