SoCal, the whole bay, a moon, and gates that actually run
Six agents in parallel, and the two city packs independently reported the same blocker: `focusRegions` and `coarseFactor` existed on the `City` type and nothing implemented them. Uniform lattices would have been 2.9M points for Southern California and 3.7M for the expanded bay. Both packs were unloadable as written. `buildAxis` is the answer, and it is honest about its limits: refinement is per axis, not per rectangle, so a focus region sharpens its whole row *and* its whole column. Two regions at opposite corners refine nearly everything between them. Measured, not guessed — the bay went 0.53M points with one region and 1.64M with three, for detail nobody is looking at from a board this wide. One region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s. Then three things that were only ever right because San Francisco was the only city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a 230-unit board; the bay is 1003 units across and the camera physically could not retreat far enough to frame it. Fog distances were scene units pinned to the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest weather, which over ninety-four kilometres of bay correctly hides three quarters of it — the night view was a black rectangle for a completely reasonable reason. All three now derive from the board. The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a physical ratio of one to four hundred thousand. What is being reproduced is what a moonlit night looks like on a screen in a lit room. The CI gate caught itself, which is the part worth keeping. Port 8431 was already held by a server from an earlier session, so the boot check polled a healthy stranger while the process it started died on EADDRINUSE. It now refuses to run rather than pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Signing in, and the things signing in must not reveal.
|
||||
*
|
||||
* The positive case is one test; the rest of this file is about the negatives,
|
||||
* because those are the ones that fail quietly in production. A wrong username
|
||||
* and a wrong password have to be the same event byte for byte. A box with no
|
||||
* local account has to look like a box with no such route. And a private office
|
||||
* has to go back to not existing the moment the cookie is cleared — a logout
|
||||
* that only hides the UI is not a logout.
|
||||
*
|
||||
* Follows `offices.test.ts`: a temp directory of office packs, `buildApp` with a
|
||||
* config built from a fake environment, and `inject()` rather than a socket.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
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 { hashPassword, parseScryptHash, verifyPassword } from "../auth/password.ts";
|
||||
import type { SessionBody } from "../routes/session.ts";
|
||||
|
||||
const SECRET = "not-a-real-secret-and-never-was";
|
||||
const USER = "karti";
|
||||
const PASSWORD = "correct horse battery staple";
|
||||
|
||||
/**
|
||||
* Produced by the exact command documented in `auth/password.ts`, pasted
|
||||
* verbatim, for the password above. It is a fixture rather than a call to
|
||||
* `hashPassword` so that the shell an operator is told to run and the parser
|
||||
* this server ships cannot drift apart without this test going red.
|
||||
*/
|
||||
const HASH_FROM_THE_DOCUMENTED_COMMAND =
|
||||
"scrypt$16384$8$1$Ns_--HDodVjObBpWzIp5TQ$_2BLRqJI14VRzr2iA-3OqR-Z1X-bVrezZYqmaU477xc";
|
||||
|
||||
/** 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-session-"));
|
||||
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 }),
|
||||
);
|
||||
});
|
||||
|
||||
const passwordEnv = {
|
||||
TERA_AUTH_MODE: "password",
|
||||
TERA_AUTH_PASSWORD_USER: USER,
|
||||
TERA_AUTH_PASSWORD_HASH: HASH_FROM_THE_DOCUMENTED_COMMAND,
|
||||
TERA_AUTH_JWT_SECRET: SECRET,
|
||||
};
|
||||
|
||||
function appWith(env: Record<string, string>) {
|
||||
const config = loadConfig({ TERA_OFFICES_DIR: dir, ...env });
|
||||
config.logLevel = "silent";
|
||||
return buildApp(config);
|
||||
}
|
||||
|
||||
function login(app: ReturnType<typeof buildApp>, username: string, password: string) {
|
||||
return app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/session",
|
||||
payload: { username, password },
|
||||
});
|
||||
}
|
||||
|
||||
/** The `Set-Cookie` value, reduced to what a browser would send back. */
|
||||
function cookiePair(setCookie: unknown): string {
|
||||
const header = Array.isArray(setCookie) ? String(setCookie[0]) : String(setCookie);
|
||||
return header.split(";")[0] ?? "";
|
||||
}
|
||||
|
||||
describe("the login endpoint", () => {
|
||||
it("accepts the right credentials and sets a locked-down cookie", async () => {
|
||||
const app = appWith(passwordEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await login(app, USER, PASSWORD);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(res.json<SessionBody>(), {
|
||||
authenticated: true,
|
||||
subject: USER,
|
||||
passwordLogin: true,
|
||||
});
|
||||
|
||||
const header = String(res.headers["set-cookie"]);
|
||||
assert.match(header, /^tera_session=[^;]+;/);
|
||||
assert.match(header, /HttpOnly/);
|
||||
assert.match(header, /Secure/);
|
||||
assert.match(header, /SameSite=Lax/);
|
||||
assert.match(header, /Path=\//);
|
||||
// Whatever else it is, it must never be a body a shared cache would keep.
|
||||
assert.equal(res.headers["cache-control"], "private, no-store");
|
||||
});
|
||||
|
||||
it("rejects a wrong password and an unknown user identically", async () => {
|
||||
const app = appWith(passwordEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const wrongPassword = await login(app, USER, "not the password");
|
||||
const unknownUser = await login(app, "someone-else", PASSWORD);
|
||||
|
||||
assert.equal(wrongPassword.statusCode, 401);
|
||||
assert.equal(unknownUser.statusCode, 401);
|
||||
assert.deepEqual(wrongPassword.json(), unknownUser.json());
|
||||
// Nothing that could be used to tell the two apart, including a cookie that
|
||||
// was set and immediately cleared.
|
||||
assert.equal(wrongPassword.headers["set-cookie"], undefined);
|
||||
assert.equal(unknownUser.headers["set-cookie"], undefined);
|
||||
});
|
||||
|
||||
it("treats a malformed body as a failed attempt rather than a hint", async () => {
|
||||
const app = appWith(passwordEnv);
|
||||
after(() => app.close());
|
||||
|
||||
for (const payload of [{}, { username: USER }, { username: 1, password: 2 }]) {
|
||||
const res = await app.inject({ method: "POST", url: "/api/v1/session", payload });
|
||||
assert.equal(res.statusCode, 401);
|
||||
}
|
||||
});
|
||||
|
||||
it("stops answering after too many failures from one address", async () => {
|
||||
const app = appWith({ ...passwordEnv, TERA_AUTH_LOGIN_ATTEMPTS: "3" });
|
||||
after(() => app.close());
|
||||
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
assert.equal((await login(app, USER, "wrong")).statusCode, 401);
|
||||
}
|
||||
|
||||
const limited = await login(app, USER, "wrong");
|
||||
assert.equal(limited.statusCode, 429);
|
||||
assert.ok(Number(limited.headers["retry-after"]) > 0);
|
||||
|
||||
// And the limit is not a way past the password: the right credentials are
|
||||
// refused too while the window is open.
|
||||
assert.equal((await login(app, USER, PASSWORD)).statusCode, 429);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a session cookie and a private office", () => {
|
||||
it("opens the private office, then closes again once the session is cleared", async () => {
|
||||
const app = appWith(passwordEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const before = await app.inject({ method: "GET", url: "/api/v1/offices/closed" });
|
||||
assert.equal(before.statusCode, 404);
|
||||
|
||||
const cookie = cookiePair((await login(app, USER, PASSWORD)).headers["set-cookie"]);
|
||||
const opened = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/offices/closed",
|
||||
headers: { cookie },
|
||||
});
|
||||
assert.equal(opened.statusCode, 200);
|
||||
assert.equal(opened.headers["cache-control"], "private, no-store");
|
||||
|
||||
const out = await app.inject({ method: "DELETE", url: "/api/v1/session", headers: { cookie } });
|
||||
assert.equal(out.statusCode, 200);
|
||||
assert.equal(out.json<SessionBody>().authenticated, false);
|
||||
// The browser is told to drop it, and the same attributes are repeated so
|
||||
// that the replacement actually matches the cookie it is replacing.
|
||||
const cleared = String(out.headers["set-cookie"]);
|
||||
assert.match(cleared, /^tera_session=;/);
|
||||
assert.match(cleared, /Max-Age=0/);
|
||||
assert.match(cleared, /HttpOnly/);
|
||||
assert.match(cleared, /Secure/);
|
||||
|
||||
// A browser that dropped the cookie is a browser with no session, and the
|
||||
// office goes back to not existing.
|
||||
const after_ = await app.inject({ method: "GET", url: "/api/v1/offices/closed" });
|
||||
assert.equal(after_.statusCode, 404);
|
||||
});
|
||||
|
||||
it("reports the session state for the client to gate its UI", async () => {
|
||||
const app = appWith(passwordEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const anonymous = await app.inject({ method: "GET", url: "/api/v1/session" });
|
||||
assert.deepEqual(anonymous.json<SessionBody>(), {
|
||||
authenticated: false,
|
||||
subject: null,
|
||||
passwordLogin: true,
|
||||
});
|
||||
|
||||
const cookie = cookiePair((await login(app, USER, PASSWORD)).headers["set-cookie"]);
|
||||
const signedIn = await app.inject({ method: "GET", url: "/api/v1/session", headers: { cookie } });
|
||||
assert.deepEqual(signedIn.json<SessionBody>(), {
|
||||
authenticated: true,
|
||||
subject: USER,
|
||||
passwordLogin: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("still keeps the token out of reach of a script", async () => {
|
||||
const app = appWith(passwordEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const cookie = cookiePair((await login(app, USER, PASSWORD)).headers["set-cookie"]);
|
||||
const state = await app.inject({ method: "GET", url: "/api/v1/session", headers: { cookie } });
|
||||
// The session body is what a page can read. It must not contain the bearer
|
||||
// token the cookie is carrying.
|
||||
assert.equal(state.body.includes(cookie.split("=")[1] ?? "never"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deployments that cannot sign anyone in", () => {
|
||||
it("leaves mode=none open and offers no login", async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
assert.equal((await app.inject({ method: "GET", url: "/api/v1/offices/open" })).statusCode, 200);
|
||||
|
||||
const state = await app.inject({ method: "GET", url: "/api/v1/session" });
|
||||
assert.deepEqual(state.json<SessionBody>(), {
|
||||
authenticated: false,
|
||||
subject: null,
|
||||
passwordLogin: false,
|
||||
});
|
||||
|
||||
// Not "login is disabled" — the same 404 an unrouted path gets.
|
||||
const attempt = await login(app, USER, PASSWORD);
|
||||
const nowhere = await app.inject({ method: "POST", url: "/api/v1/no-such-thing" });
|
||||
assert.equal(attempt.statusCode, 404);
|
||||
assert.deepEqual(attempt.json(), nowhere.json());
|
||||
});
|
||||
|
||||
it("demotes to mode=none when the hash is unreadable, and says so", async () => {
|
||||
const config = loadConfig({
|
||||
TERA_OFFICES_DIR: dir,
|
||||
...passwordEnv,
|
||||
TERA_AUTH_PASSWORD_HASH: "scrypt$notanumber$8$1$aaaa$bbbb",
|
||||
});
|
||||
config.logLevel = "silent";
|
||||
const app = buildApp(config);
|
||||
after(() => app.close());
|
||||
|
||||
assert.equal(config.auth.mode, "none");
|
||||
assert.equal(config.auth.passwordLogin, null);
|
||||
assert.ok(config.degraded.some((line) => line.includes("TERA_AUTH_PASSWORD_HASH")));
|
||||
// The demotion runs towards closed: nobody gets the private office.
|
||||
assert.equal((await app.inject({ method: "GET", url: "/api/v1/offices/closed" })).statusCode, 404);
|
||||
assert.equal((await login(app, USER, PASSWORD)).statusCode, 404);
|
||||
});
|
||||
|
||||
it("demotes when there is no secret to sign a session with", async () => {
|
||||
const config = loadConfig({
|
||||
TERA_AUTH_MODE: "password",
|
||||
TERA_AUTH_PASSWORD_USER: USER,
|
||||
TERA_AUTH_PASSWORD_HASH: HASH_FROM_THE_DOCUMENTED_COMMAND,
|
||||
});
|
||||
assert.equal(config.auth.mode, "none");
|
||||
assert.ok(config.degraded.some((line) => line.includes("TERA_AUTH_JWT_SECRET")));
|
||||
});
|
||||
|
||||
it("resolves password mode to jwt so there is one authorisation path", async () => {
|
||||
const config = loadConfig(passwordEnv);
|
||||
assert.equal(config.auth.mode, "jwt");
|
||||
assert.equal(config.auth.jwtVerify, "hs256");
|
||||
assert.equal(config.auth.passwordLogin?.username, USER);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the hash format", () => {
|
||||
it("reads what the documented one-liner writes", async () => {
|
||||
const parsed = parseScryptHash(HASH_FROM_THE_DOCUMENTED_COMMAND);
|
||||
assert.notEqual(parsed, null);
|
||||
assert.equal(parsed?.n, 16384);
|
||||
assert.equal(parsed?.r, 8);
|
||||
assert.equal(parsed?.p, 1);
|
||||
assert.equal(await verifyPassword(parsed!, PASSWORD), true);
|
||||
assert.equal(await verifyPassword(parsed!, "close but no"), false);
|
||||
});
|
||||
|
||||
it("round-trips a freshly generated hash, with a different salt each time", async () => {
|
||||
const first = await hashPassword(PASSWORD);
|
||||
const second = await hashPassword(PASSWORD);
|
||||
assert.notEqual(first, second);
|
||||
|
||||
const parsed = parseScryptHash(first);
|
||||
assert.notEqual(parsed, null);
|
||||
assert.equal(await verifyPassword(parsed!, PASSWORD), true);
|
||||
});
|
||||
|
||||
it("refuses anything it cannot verify against", () => {
|
||||
for (const bad of [
|
||||
"",
|
||||
"hunter2",
|
||||
"bcrypt$16384$8$1$aaaa$bbbb",
|
||||
"scrypt$16384$8$1$aaaa",
|
||||
"scrypt$16383$8$1$c21d5m2wtoFIu4-rXO6mXA$aaaaaaaaaaaaaaaaaaaaaaaa", // N not a power of two
|
||||
"scrypt$1073741824$8$1$c21d5m2wtoFIu4-rXO6mXA$aaaaaaaaaaaaaaaaaaaaaaaa", // absurd memory
|
||||
"scrypt$16384$8$1$not base64!$aaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"scrypt$16384$8$1$c21d5m2wtoFIu4-rXO6mXA$aa", // key too short to be a key
|
||||
]) {
|
||||
assert.equal(parseScryptHash(bad), null, `${bad} must not parse`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user