/** * T-013 — GET /api/settings/authz-inventory. * * Covers spec AC-1, AC-2, AC-12 (and AC-13 PATCH coverage continues to live * in `tests/unit/settings/authz-bypass.test.ts`; this file only asserts the * inventory endpoint itself). * * - AC-1 response shape: 5 tiers, each with prefixes; bypassEnabled / bypassPrefixes / spawnCapablePrefixes present. * - AC-2 bypass-state flags match getSettings() and update after PATCH. * - AC-12 anonymous request → 401/403 (no inventory leak). */ import test from "node:test"; import assert from "node:assert/strict"; import { setupSettingsFixture } from "../_mocks/settings.ts"; import { makeManagementSessionRequest } from "../../helpers/managementSession.ts"; const fixture = setupSettingsFixture("authz-inventory"); process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1"; const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; const ORIGINAL_CORS_ALLOW_ALL = process.env.CORS_ALLOW_ALL; const core = await import("../../../src/lib/db/core.ts"); const settingsDb = await import("../../../src/lib/db/settings.ts"); const runtime = await import("../../../src/lib/config/runtimeSettings.ts"); const inventoryRoute = await import("../../../src/app/api/settings/authz-inventory/route.ts"); const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); test.beforeEach(async () => { await fixture.resetStorage(); apiKeysDb.resetApiKeyState(); runtime.resetRuntimeSettingsStateForTests(); }); test.after(() => { core.resetDbInstance(); fixture.cleanup(); if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET; else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET; if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD; else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; if (ORIGINAL_CORS_ALLOW_ALL === undefined) delete process.env.CORS_ALLOW_ALL; else process.env.CORS_ALLOW_ALL = ORIGINAL_CORS_ALLOW_ALL; }); // ─── AC-1 — shape ───────────────────────────────────────────────────────── test("AC-1: GET returns 5 tiers with prefixes + bypass state envelope", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-ac1"; await settingsDb.updateSettings({ requireLogin: true }); const request = await makeManagementSessionRequest( "http://localhost/api/settings/authz-inventory", { method: "GET" } ); const response = await inventoryRoute.GET(request); assert.equal(response.status, 200); const body = (await response.json()) as { tiers: Array<{ name: string; prefixes: string[]; description: string; bypassable: boolean }>; bypassEnabled: boolean; bypassPrefixes: string[]; spawnCapablePrefixes: string[]; }; assert.equal(body.tiers.length, 5); const names = body.tiers.map((t) => t.name).sort(); assert.deepEqual(names, ["ALWAYS_PROTECTED", "CLIENT_API", "LOCAL_ONLY", "MANAGEMENT", "PUBLIC"]); const localOnly = body.tiers.find((t) => t.name === "LOCAL_ONLY"); assert.ok(localOnly); assert.ok(localOnly!.prefixes.includes("/api/mcp/")); assert.ok(localOnly!.prefixes.includes("/api/cli-tools/runtime/")); assert.equal(localOnly!.bypassable, true); const alwaysProtected = body.tiers.find((t) => t.name === "ALWAYS_PROTECTED"); assert.ok(alwaysProtected); assert.ok(alwaysProtected!.prefixes.includes("/api/shutdown")); assert.equal(alwaysProtected!.bypassable, false); const clientApi = body.tiers.find((t) => t.name === "CLIENT_API"); assert.ok(clientApi); assert.ok(clientApi!.prefixes.includes("/v1/")); assert.ok(clientApi!.prefixes.includes("/api/v1/")); assert.ok(clientApi!.prefixes.includes("/v1beta/")); assert.ok(clientApi!.prefixes.includes("/api/v1beta/")); // Every tier carries a non-empty description. for (const tier of body.tiers) { assert.ok(tier.description.length > 0, `tier ${tier.name} missing description`); } assert.ok(body.spawnCapablePrefixes.includes("/api/cli-tools/runtime/")); }); // ─── AC-2 — flags match getSettings() pre- and post-mutation ────────────── test("AC-2: bypassEnabled + bypassPrefixes reflect getSettings() (defaults)", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-ac2a"; await settingsDb.updateSettings({ requireLogin: true }); const response = await inventoryRoute.GET( await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { method: "GET", }) ); assert.equal(response.status, 200); const body = (await response.json()) as { bypassEnabled: boolean; bypassPrefixes: string[]; }; // Default snapshot: kill-switch ON, single prefix /api/mcp/. assert.equal(body.bypassEnabled, true); assert.deepEqual(body.bypassPrefixes, ["/api/mcp/"]); }); test("AC-2: bypassEnabled flips after settings mutation", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-ac2b"; await settingsDb.updateSettings({ requireLogin: true }); // Mutate directly through the settings DB (bypasses the password gate — // we are not testing the gate here, only the inventory's reflection of // the persisted state). await settingsDb.updateSettings({ localOnlyManageScopeBypassEnabled: false }); const response = await inventoryRoute.GET( await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { method: "GET", }) ); assert.equal(response.status, 200); const body = (await response.json()) as { bypassEnabled: boolean }; assert.equal(body.bypassEnabled, false); }); test("AC-2: bypassPrefixes additions land in the inventory", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-ac2c"; await settingsDb.updateSettings({ requireLogin: true }); await settingsDb.updateSettings({ localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/mcp/v2/"], }); const response = await inventoryRoute.GET( await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { method: "GET", }) ); assert.equal(response.status, 200); const body = (await response.json()) as { bypassPrefixes: string[] }; assert.deepEqual(body.bypassPrefixes, ["/api/mcp/", "/api/mcp/v2/"]); }); // ─── #5602 — CORS status surfaced for the dashboard wildcard warning ────── test("#5602: cors.allowAll is false by default (no CORS_ALLOW_ALL)", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-cors-a"; delete process.env.CORS_ALLOW_ALL; await settingsDb.updateSettings({ requireLogin: true }); const response = await inventoryRoute.GET( await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { method: "GET", }) ); assert.equal(response.status, 200); const body = (await response.json()) as { cors: { allowAll: boolean; allowedOrigins: string[] }; }; assert.ok(body.cors, "response should carry a cors envelope"); assert.equal(body.cors.allowAll, false); assert.deepEqual(body.cors.allowedOrigins, []); }); test("#5602: cors.allowAll reflects CORS_ALLOW_ALL=true", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-cors-b"; process.env.CORS_ALLOW_ALL = "true"; await settingsDb.updateSettings({ requireLogin: true }); const response = await inventoryRoute.GET( await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { method: "GET", }) ); assert.equal(response.status, 200); const body = (await response.json()) as { cors: { allowAll: boolean } }; assert.equal(body.cors.allowAll, true); }); // ─── AC-12 — anonymous request rejected (no inventory leak) ─────────────── test("AC-12: anonymous request (no cookie, no Bearer) → 401", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-ac12"; // Bootstrap a password so isAuthRequired() returns true even on loopback. await settingsDb.updateSettings({ requireLogin: true }); const { ensurePersistentManagementPasswordHash } = await import("../../../src/lib/auth/managementPassword.ts"); await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); const anonRequest = new Request("https://dashboard.example/api/settings/authz-inventory", { method: "GET", }); const response = await inventoryRoute.GET(anonRequest); assert.ok( response.status === 401 || response.status === 403, `expected 401/403, got ${response.status}` ); const body = (await response.json()) as { error?: { message?: string } }; // Should NOT leak the inventory shape. assert.ok(!("tiers" in body)); }); test("AC-12: anonymous request with bogus Bearer → 403", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-ac12b"; await settingsDb.updateSettings({ requireLogin: true }); const { ensurePersistentManagementPasswordHash } = await import("../../../src/lib/auth/managementPassword.ts"); await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); const bogus = new Request("https://dashboard.example/api/settings/authz-inventory", { method: "GET", headers: new Headers({ authorization: "Bearer not-a-real-key" }), }); const response = await inventoryRoute.GET(bogus); assert.equal(response.status, 403); }); // ─── OQ-5 — any valid API key (no manage scope required) → 200 ──────────── test("OQ-5: any valid API key (read-only scope) → 200 inventory", async () => { process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; process.env.INITIAL_PASSWORD = "initial-pass-oq5"; await settingsDb.updateSettings({ requireLogin: true }); const { ensurePersistentManagementPasswordHash } = await import("../../../src/lib/auth/managementPassword.ts"); await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); // Key with NO manage scope — would be rejected by /api/settings PATCH, // but the inventory read endpoint is intentionally one rung lower (OQ-5). const created = await apiKeysDb.createApiKey("oq5-read", "machine-oq5", []); const request = new Request("https://dashboard.example/api/settings/authz-inventory", { method: "GET", headers: new Headers({ authorization: `Bearer ${created.key}` }), }); const response = await inventoryRoute.GET(request); assert.equal(response.status, 200); const body = (await response.json()) as { tiers: unknown[] }; assert.equal(body.tiers.length, 5); });