import assert from "node:assert/strict"; import test from "node:test"; import { createFoundryAuth } from "./nodedc-auth.mjs"; const HANDOFF_TOKEN = "handoff-token-must-never-appear-in-diagnostics"; const LAUNCHER_SESSION_ID = "launcher-session-must-never-appear-in-diagnostics"; const INTERNAL_ACCESS_TOKEN = "internal-access-token-must-never-appear-in-diagnostics"; const USER = Object.freeze({ id: "user_root", email: "root@example.test", name: "DC SUDO", groups: ["nodedc:superadmin"], }); test("Foundry session validation is burst-safe, failure-tolerant and revocation-bounded", async (t) => { const restoreEnvironment = setEnvironment({ NODE_ENV: "production", NODEDC_FOUNDRY_AUTH_REQUIRED: "true", NODEDC_FOUNDRY_COOKIE_SECURE: "false", NODEDC_FOUNDRY_SESSION_TTL_MS: String(60 * 60 * 1000), NODEDC_LAUNCHER_BASE_URL: "https://launcher.example.test", NODEDC_LAUNCHER_INTERNAL_URL: "http://launcher.internal.test", NODEDC_INTERNAL_ACCESS_TOKEN: INTERNAL_ACCESS_TOKEN, }); t.after(restoreEnvironment); let clock = 1_000_000; let validationMode = "active"; let validationCalls = 0; const diagnostics = []; const fetchStub = async (url, init) => { assert.equal(init.headers.Authorization, `Bearer ${INTERNAL_ACCESS_TOKEN}`); const pathname = new URL(url).pathname; const payload = JSON.parse(init.body); if (pathname === "/api/internal/handoff/consume") { assert.equal(payload.token, HANDOFF_TOKEN); return jsonResponse(200, { ok: true, launcherSessionId: LAUNCHER_SESSION_ID, user: USER, }); } assert.equal(pathname, "/api/internal/session/validate"); assert.equal(payload.launcherSessionId, LAUNCHER_SESSION_ID); validationCalls += 1; // Let every request in a simulated Cesium tile burst reach the shared // validation promise before it resolves. await new Promise((resolve) => setImmediate(resolve)); if (validationMode === "transient") { return jsonResponse(503, { ok: false, active: false, error: "launcher_temporarily_unavailable" }); } if (validationMode === "network") { throw new TypeError(`network failed ${INTERNAL_ACCESS_TOKEN}`); } if (validationMode === "timeout") { const error = new Error(`timeout ${LAUNCHER_SESSION_ID}`); error.name = "TimeoutError"; throw error; } if (validationMode === "inactive") { return jsonResponse(200, { ok: true, active: false, error: "service_access_denied" }); } return jsonResponse(200, { ok: true, active: true, user: USER }); }; const auth = createFoundryAuth({ now: () => clock, fetch: fetchStub, validationTtlMs: 20, validationGraceMs: 30, validationRetryMs: 5, onDiagnostic: (record) => diagnostics.push(record), }); const cookie = await createSessionCookie(auth); await t.test("concurrent tile burst performs one Launcher validation", async () => { clock += 21; const results = await Promise.all( Array.from({ length: 64 }, (_, index) => authorize(auth, cookie, `/api/map-gateway/api/map/cache?tile=${index}`)), ); assert.equal(validationCalls, 1); assert.ok(results.every(({ handled }) => handled === false)); assert.ok(results.every(({ response }) => response.getHeader("set-cookie") === undefined)); const withinTtl = await Promise.all( Array.from({ length: 64 }, (_, index) => authorize(auth, cookie, `/api/map-gateway/api/map/cache?warm=${index}`)), ); assert.equal(validationCalls, 1); assert.ok(withinTtl.every(({ handled }) => handled === false)); }); await t.test("transient Launcher failure keeps the cookie and uses bounded read-only grace", async () => { validationMode = "transient"; clock += 21; const graceResults = await Promise.all( Array.from({ length: 32 }, (_, index) => authorize(auth, cookie, `/api/map-gateway/api/map/cache?grace=${index}`)), ); assert.equal(validationCalls, 2); assert.ok(graceResults.every(({ handled }) => handled === false)); assert.ok(graceResults.every(({ response }) => response.getHeader("set-cookie") === undefined)); const mutation = await authorize(auth, cookie, "/api/platform-settings/cesium-ion", "PUT"); assert.equal(mutation.handled, true); assert.equal(mutation.response.statusCode, 503); assert.equal(JSON.parse(mutation.response.body).error, "module_foundry_auth_unavailable"); assert.equal(mutation.response.getHeader("set-cookie"), undefined); // The short retry backoff shares the transient result instead of creating a // control-plane retry storm from the same burst. assert.equal(validationCalls, 2); clock += 31; const outsideGrace = await authorize(auth, cookie, "/api/map/runtime-config"); assert.equal(outsideGrace.handled, true); assert.equal(outsideGrace.response.statusCode, 503); assert.equal(JSON.parse(outsideGrace.response.body).error, "module_foundry_auth_unavailable"); assert.equal(outsideGrace.response.getHeader("set-cookie"), undefined); assert.equal(validationCalls, 3); }); await t.test("network errors and timeouts keep the last-known-good read-only session", async () => { validationMode = "active"; clock += 6; const recovered = await authorize(auth, cookie, "/api/map/runtime-config"); assert.equal(recovered.handled, false); assert.equal(validationCalls, 4); validationMode = "network"; clock += 21; const networkFailure = await authorize(auth, cookie, "/api/map/runtime-config"); assert.equal(networkFailure.handled, false); assert.equal(networkFailure.response.getHeader("set-cookie"), undefined); assert.equal(validationCalls, 5); validationMode = "timeout"; clock += 6; const timeout = await authorize(auth, cookie, "/api/map/runtime-config"); assert.equal(timeout.handled, false); assert.equal(timeout.response.getHeader("set-cookie"), undefined); assert.equal(validationCalls, 6); }); await t.test("explicit Launcher inactive response revokes the local session", async () => { validationMode = "inactive"; clock += 6; const revoked = await authorize(auth, cookie, "/api/map/runtime-config"); assert.equal(revoked.handled, true); assert.equal(revoked.response.statusCode, 401); assert.equal(JSON.parse(revoked.response.body).error, "module_foundry_auth_required"); assert.match(String(revoked.response.getHeader("set-cookie")), /Max-Age=0/); assert.equal(validationCalls, 7); const afterRevoke = await authorize(auth, cookie, "/api/map/runtime-config"); assert.equal(afterRevoke.handled, true); assert.equal(afterRevoke.response.statusCode, 401); assert.equal(validationCalls, 7); }); await t.test("diagnostics contain outcomes but no cookie, Launcher id or token", () => { assert.ok(diagnostics.some((record) => record.outcome === "active")); assert.ok(diagnostics.some((record) => record.outcome === "transient")); assert.ok(diagnostics.some((record) => record.outcome === "inactive")); const serialized = JSON.stringify(diagnostics); assert.equal(serialized.includes(cookie), false); assert.equal(serialized.includes(HANDOFF_TOKEN), false); assert.equal(serialized.includes(LAUNCHER_SESSION_ID), false); assert.equal(serialized.includes(INTERNAL_ACCESS_TOKEN), false); assert.equal(serialized.includes(USER.email), false); }); await t.test("a process restart expires an opaque process-local cookie safely", async () => { let restartedValidationCalls = 0; const restartDiagnostics = []; const restartedAuth = createFoundryAuth({ now: () => clock, fetch: async () => { restartedValidationCalls += 1; throw new Error("must not validate an unrecoverable local cookie"); }, validationTtlMs: 20, validationGraceMs: 30, validationRetryMs: 5, onDiagnostic: (record) => restartDiagnostics.push(record), }); const result = await authorize(restartedAuth, cookie, "/api/map/runtime-config"); assert.equal(result.handled, true); assert.equal(result.response.statusCode, 401); assert.match(String(result.response.getHeader("set-cookie")), /Max-Age=0/); assert.equal(restartedValidationCalls, 0); assert.ok(restartDiagnostics.some((record) => record.reason === "local_session_missing_or_expired")); }); }); async function createSessionCookie(auth) { const request = { method: "GET", url: `/auth/nodedc/handoff?token=${encodeURIComponent(HANDOFF_TOKEN)}&returnTo=%2F`, headers: {}, }; const response = mockResponse(); await auth.handleLauncherHandoff(request, response, new URL(request.url, "https://foundry.example.test")); assert.equal(response.statusCode, 302); const setCookie = String(response.getHeader("set-cookie")); assert.match(setCookie, /^nodedc_foundry_session=/); return setCookie.split(";", 1)[0]; } async function authorize(auth, cookie, path, method = "GET") { const request = { method, url: path, headers: { cookie, accept: "application/json" } }; const response = mockResponse(); const handled = await auth.ensureRequestSession( request, response, new URL(path, "https://foundry.example.test"), ); return { handled, request, response }; } function mockResponse() { const headers = new Map(); return { statusCode: 200, body: "", setHeader(name, value) { headers.set(String(name).toLowerCase(), value); }, getHeader(name) { return headers.get(String(name).toLowerCase()); }, end(body = "") { this.body = String(body); }, }; } function jsonResponse(status, body) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }); } function setEnvironment(values) { const original = Object.fromEntries(Object.keys(values).map((key) => [key, process.env[key]])); for (const [key, value] of Object.entries(values)) process.env[key] = value; return () => { for (const [key, value] of Object.entries(original)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }; }