/** * Offices, visibility, and the 404 rule. * * The assertion that matters is the negative one: a private office and an office * that was never created must be indistinguishable from outside. If they differ * — by status code, by body, by timing anybody could measure — the endpoint * becomes a way to enumerate tenants. CONTRACT.md §6. * * HS256 is exercised directly here because it is the primary path: the issuer * this runs against signs `{"alg":"HS256"}`, and a JWKS-only implementation * would reject every real token. */ import assert from "node:assert/strict"; import { createHmac } from "node:crypto"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { after, before, describe, it } from "node:test"; import { buildApp } from "../app.ts"; import { loadConfig } from "../config.ts"; import type { OfficeDoc } from "../../../src/server/wire.ts"; const SECRET = "not-a-real-secret-and-never-was"; /** A minimal floor. `Plan` is what makes sense of it; the API only carries it. */ const floor = { id: "hq", name: "HQ", levels: [], viewpoints: [] }; let dir = ""; before(async () => { dir = await mkdtemp(join(tmpdir(), "tera-offices-")); await writeFile( join(dir, "open.json"), JSON.stringify({ id: "open", name: "Open office", visibility: "public", floor }), ); await writeFile( join(dir, "closed.json"), JSON.stringify({ id: "closed", name: "Closed office", visibility: "private", floor }), ); await writeFile( join(dir, "quiet.json"), JSON.stringify({ id: "quiet", name: "Unlisted office", visibility: "unlisted", floor }), ); // No `visibility` at all: the fail-closed reading is `private`. await writeFile(join(dir, "vague.json"), JSON.stringify({ id: "vague", name: "?", floor })); }); function appWith(env: Record) { const config = loadConfig({ TERA_OFFICES_DIR: dir, ...env }); config.logLevel = "silent"; return buildApp(config); } function hs256(claims: Record): string { const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url"); const signed = `${encode({ alg: "HS256", typ: "JWT" })}.${encode(claims)}`; return `${signed}.${createHmac("sha256", SECRET).update(signed).digest("base64url")}`; } describe("with auth off", () => { it("serves a public office and lets it be cached", async () => { const app = appWith({}); after(() => app.close()); const res = await app.inject({ method: "GET", url: "/api/v1/offices/open" }); assert.equal(res.statusCode, 200); assert.equal(res.json().floor.id, "hq"); assert.match(String(res.headers["cache-control"]), /^public, max-age=/); }); it("serves an unlisted office but never lets a shared cache keep it", async () => { const app = appWith({}); after(() => app.close()); const res = await app.inject({ method: "GET", url: "/api/v1/offices/quiet" }); assert.equal(res.statusCode, 200); assert.equal(res.headers["cache-control"], "private, no-store"); }); it("answers 404 for a private office, identically to one that does not exist", async () => { const app = appWith({}); after(() => app.close()); const priv = await app.inject({ method: "GET", url: "/api/v1/offices/closed" }); const absent = await app.inject({ method: "GET", url: "/api/v1/offices/no-such-office" }); assert.equal(priv.statusCode, 404); assert.equal(absent.statusCode, 404); assert.deepEqual(priv.json(), absent.json()); }); it("treats a pack that forgot to declare visibility as private", async () => { const app = appWith({}); after(() => app.close()); assert.equal((await app.inject({ method: "GET", url: "/api/v1/offices/vague" })).statusCode, 404); }); it("refuses an id that could climb out of the directory", async () => { const app = appWith({}); after(() => app.close()); for (const id of ["..", "..%2f..%2fetc%2fpasswd", "Open", "open.json"]) { const res = await app.inject({ method: "GET", url: `/api/v1/offices/${id}` }); assert.equal(res.statusCode, 404, `${id} must not resolve`); } }); }); describe("with TERA_AUTH_MODE=jwt", () => { const env = { TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET }; it("opens a private office to a valid HS256 token", async () => { const app = appWith(env); after(() => app.close()); const res = await app.inject({ method: "GET", url: "/api/v1/offices/closed", headers: { authorization: `Bearer ${hs256({ sub: "someone", exp: now() + 600 })}` }, }); assert.equal(res.statusCode, 200); assert.equal(res.headers["cache-control"], "private, no-store"); }); it("still answers 404 to an expired token, a wrong secret, or alg: none", async () => { const app = appWith(env); after(() => app.close()); const expired = hs256({ sub: "someone", exp: now() - 3600 }); const wrong = `${hs256({ sub: "someone" }).split(".").slice(0, 2).join(".")}.deadbeef`; const none = `${Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")}.${Buffer.from( JSON.stringify({ sub: "someone" }), ).toString("base64url")}.`; for (const token of [expired, wrong, none, "not-a-jwt"]) { const res = await app.inject({ method: "GET", url: "/api/v1/offices/closed", headers: { authorization: `Bearer ${token}` }, }); assert.equal(res.statusCode, 404); } }); }); function now(): number { return Math.floor(Date.now() / 1000); }