feat(foundry): productionize Cesium Map Page and platform runtime
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user