d464459838
Ten agents wrote this in parallel against CONTRACT.md, which exists because the five design agents before them collided on fifteen blocking points — four files specified twice with incompatible contents, three separate backends for one box, and `Environment` exported twice meaning different things. What landed: a Stage owning only the renderer and the loop, with the city and an office as two scenes over it. They cannot share one — San Francisco is ~94 m per scene unit with 3.6x vertical exaggeration and an office is 1 unit = 1 m — and the city is paused rather than disposed on the way in, because rebuilding its 336,864-point heightfield costs about a second on the way back out. Offices are data. `src/offices/lumbridge-hq.ts` is fifteen rooms and seventy-six seats, and it is the file a self-hoster copies. Walls are a segment list with 1-D openings, so doors and windows are holes punched in a wall rather than placed objects, and the pass that splits a wall around its openings hands the walk-mode collider its segments for free. The sun is real. `solar.ts` is a NOAA/Meeus implementation with no imports at all — not even three.js — so time of day keeps working on a laptop in a field. Verified against known values: 75.45 degrees at the June solstice in SF, 28.79 at December, sunset at 03:15Z. The first screenshot after wiring it was a black rectangle, which turned out to be correct: it was midnight in San Francisco. Presence binds to a seat id and never to a coordinate. The pack knows where `eng-04` is; who is sitting in it is private data behind an API. Same shape as the marker rule, one level in. Two corrections to ARCHITECTURE.md are in here. Containment does not discharge ODbL — publishing OSM-derived coordinates is Public Use of a Derivative Database wherever the rows live, so the rule is about the geocoder (US Census, public domain) and not the storage. And a person at a desk is not a Marker; markers are geographic. One contract gap surfaced only in a screenshot: two agents read `height` on a viewpoint differently, so the establishing shot aimed at empty air fourteen metres above the roof. It now means what the same field means for a city. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.5 KiB
TypeScript
150 lines
5.5 KiB
TypeScript
/**
|
|
* 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<string, string>) {
|
|
const config = loadConfig({ TERA_OFFICES_DIR: dir, ...env });
|
|
config.logLevel = "silent";
|
|
return buildApp(config);
|
|
}
|
|
|
|
function hs256(claims: Record<string, unknown>): 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<OfficeDoc>().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);
|
|
}
|