import { test } from "node:test"; import assert from "node:assert/strict"; import { createAuthGateway } from "../src/lib/auth-gateway.mjs"; import { COOKIE_NAME } from "../src/lib/auth-policy.mjs"; const origin = "http://localhost:17322"; const config = { project: "thesisly", appOrigin: origin, apiOrigin: "https://core.example.test" }; function request(path, { method = "POST", headers = {}, body = "{}" } = {}) { return new Request(`${origin}/api/foundry/${path}`, { method, headers: { origin, "content-type": "application/json", ...headers }, ...(method === "GET" ? {} : { body }) }); } test("exchange keeps tokens in Cookie and forwards only approved headers", async () => { const gateway = createAuthGateway(config, async (url, options) => { assert.equal(url, "https://core.example.test/projects/thesisly/auth/exchange"); assert.equal(options.headers.get("x-foundry-code"), "single-use-code"); assert.equal(options.headers.get("x-foundry-verifier"), "tab-verifier"); assert.equal(options.headers.get("authorization"), null); assert.equal(options.headers.get("cookie"), null); assert.equal(options.redirect, "error"); assert.equal(options.cache, "no-store"); return Response.json({ access_token: "session-token", refresh_token: "never-expose", expires_in: 90 }); }); const response = await gateway(request("auth/exchange", { headers: { "x-foundry-code": "single-use-code", "x-foundry-verifier": "tab-verifier", authorization: "Bearer injected", cookie: "other_app_session=private" } }), "auth/exchange"); assert.equal(response.status, 200); assert.match(response.headers.get("set-cookie"), /session-token; HttpOnly; SameSite=Lax; Path=\/; Max-Age=90$/); assert.deepEqual(await response.json(), { expires_in: 90 }); assert.equal(response.headers.get("cache-control"), "no-store"); }); test("login and email verification require usable session responses", async () => { for (const path of ["auth/login", "auth/verify-email"]) { const valid = createAuthGateway({ ...config, appOrigin: "https://thesisly.example.test" }, async () => Response.json({ access_token: "token", expires_in: 90000000 })); const ok = await valid(request(path, { headers: { origin: "https://thesisly.example.test" } }), path); assert.equal(ok.status, 200); assert.match(ok.headers.get("set-cookie"), /Max-Age=2592000; Secure$/); for (const payload of [{ access_token: "token" }, { access_token: "token", expires_in: 0 }, { expires_in: 100 }, null]) { const response = await createAuthGateway(config, async () => Response.json(payload))(request(path), path); assert.equal(response.status, 502); assert.equal(response.headers.get("set-cookie"), null); assert.deepEqual(await response.json(), { error: "invalid_session_response" }); } } }); test("registration may request email verification without issuing a session", async () => { const response = await createAuthGateway(config, async () => Response.json({ status: "verification_required" }, { status: 202 }))(request("auth/register"), "auth/register"); assert.equal(response.status, 202); assert.equal(response.headers.get("set-cookie"), null); }); test("restore uses only the site Cookie and honors the short renewal deadline", async () => { const gateway = createAuthGateway(config, async (_url, options) => { assert.equal(options.headers.get("authorization"), "Bearer stored-token"); assert.equal(options.headers.get("cookie"), null); return Response.json({ id: "account-id", email: null }, { headers: { "X-Foundry-Session-Expires-In": "7" } }); }); const response = await gateway(request("auth/me", { method: "GET", headers: { cookie: `${COOKIE_NAME}=stored-token` } }), "auth/me"); assert.equal(response.status, 200); assert.match(response.headers.get("set-cookie"), /Max-Age=7$/); }); test("logout revokes before clearing; upstream failure preserves the local Cookie", async () => { const req = () => request("auth/logout", { headers: { cookie: `${COOKIE_NAME}=stored-token` } }); const done = await createAuthGateway(config, async () => new Response(null, { status: 204 }))(req(), "auth/logout"); assert.equal(done.status, 204); assert.match(done.headers.get("set-cookie"), /Max-Age=0$/); const failed = await createAuthGateway(config, async () => { throw new Error("offline"); })(req(), "auth/logout"); assert.equal(failed.status, 502); assert.equal(failed.headers.get("set-cookie"), null); }); test("401 clears an expired session and strips unexpected refresh tokens", async () => { const gateway = createAuthGateway(config, async () => Response.json({ error: "invalid_session", refresh_token: "private" }, { status: 401 })); const response = await gateway(request("auth/me", { method: "GET", headers: { cookie: `${COOKIE_NAME}=expired` } }), "auth/me"); assert.match(response.headers.get("set-cookie"), /Max-Age=0$/); assert.deepEqual(await response.json(), { error: "invalid_session" }); }); test("invalid paths, methods, origins, cookies and bodies never reach Core", async () => { let calls = 0; const gateway = createAuthGateway(config, async () => { ++calls; throw new Error("must not call"); }); const cases = [ ["canvas", request("canvas"), 404], ["auth/login", request("auth/login", { method: "GET" }), 405], ["auth/login", request("auth/login", { headers: { origin: "https://other.example.test" } }), 403], ["auth/login", request("auth/login", { headers: { "sec-fetch-site": "cross-site" } }), 403], ["auth/login", request("auth/login", { body: "not json" }), 400], ["auth/login", request("auth/login", { body: JSON.stringify({ data: "x".repeat(16384) }) }), 413], ["auth/me", request("auth/me", { method: "GET" }), 401], ["auth/me", request("auth/me", { method: "GET", headers: { cookie: `${COOKIE_NAME}=%E0%A4%A` } }), 401], ]; for (const [path, req, expected] of cases) assert.equal((await gateway(req, path)).status, expected); assert.equal(calls, 0); }); test("rate limits retain Retry-After without minting or exposing credentials", async () => { const gateway = createAuthGateway(config, async () => Response.json({ error: "rate_limited", access_token: "private", refresh_token: "private" }, { status: 429, headers: { "retry-after": "30" } })); const response = await gateway(request("auth/login"), "auth/login"); assert.equal(response.status, 429); assert.equal(response.headers.get("retry-after"), "30"); assert.equal(response.headers.get("set-cookie"), null); assert.deepEqual(await response.json(), { error: "rate_limited" }); });