Spaces:
Runtime error
Runtime error
| 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<string> = [ | |
| "/api/settings", | |
| "/api/providers/", | |
| "/api/api-keys", | |
| ]; | |
| const CLIENT_API_TIER_PREFIXES: ReadonlyArray<string> = ["/v1/", "/api/v1/"]; | |
| const PUBLIC_TIER_PREFIXES: ReadonlyArray<string> = ["/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<Response | null> { | |
| 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 }); | |
| } | |
| } | |