1
0

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:
Karti Tripathi
2026-08-05 03:13:32 -07:00
parent 8bcb391455
commit 44c5a79424
25 changed files with 9246 additions and 220 deletions
+215
View File
@@ -0,0 +1,215 @@
/**
* `/api/v1/session` — the only place this box issues a credential rather than
* checking one.
*
* It exists because neither committed mode lets a human sign in: `none` is open
* and `jwt` verifies a token somebody else already issued. `TERA_AUTH_MODE=password`
* fills that gap for a self-hoster with no identity provider, and it fills it by
* signing the same HS256 token `jwt` mode verifies and putting it in a cookie.
* `offices.ts` is unchanged and unaware; there is still exactly one code path
* that decides whether a private office exists for you.
*
* Three deliberate refusals to be helpful, all of them about not answering
* questions an anonymous caller should not get answers to:
*
* - A wrong username and a wrong password produce the same status, the same body
* and the same amount of work. `credentialsMatch` in `auth/password.ts` is
* where that is enforced.
* - When this box cannot sign anyone in, `POST` answers **404 with the body an
* unrouted path gets**, not "password login is disabled". Whether an operator
* configured a local account is not public information.
* - A private office still answers 404 rather than 403, before and after signing
* in. That rule lives in `offices.ts` and nothing here weakens it.
*/
import type { FastifyInstance } from "fastify";
import {
clearedSessionCookie,
issueSessionToken,
sessionCookie,
} from "../auth/index.ts";
import { MAX_PASSWORD_LENGTH, credentialsMatch } from "../auth/password.ts";
import type { ErrorBody } from "../../../src/server/wire.ts";
import type { Services } from "../services.ts";
/**
* What the client gets to know. `passwordLogin` is the field the UI gates on:
* it says a login form would do something here, which is not a secret — the
* login page is served to anyone — while `authenticated` says whether this
* particular caller needs one.
*/
export interface SessionBody {
authenticated: boolean;
/** The signed-in subject, or `null`. Never a token. */
subject: string | null;
/** Whether `POST` to this endpoint can sign somebody in on this deployment. */
passwordLogin: boolean;
}
/** Byte-identical to what the not-found handler serves for a path with no route. */
const NOT_FOUND: ErrorBody = { error: "not_found", message: "No such route." };
/**
* One message for every rejected credential. It says nothing about which half
* was wrong, and the same object is sent for a username that does not exist.
*/
const REJECTED: ErrorBody = {
error: "unauthorized",
message: "Those credentials were not accepted.",
};
/**
* `unauthorized` rather than a token of its own because the error union in
* `wire.ts` is closed and deliberately short. The 429 and `Retry-After` carry
* the distinction, and a client that only reads `error` treats a rate-limited
* attempt as a failed one, which is the right thing for it to do.
*/
const RATE_LIMITED: ErrorBody = {
error: "unauthorized",
message: "Too many sign-in attempts. Try again shortly.",
};
const MAX_USERNAME_LENGTH = 256;
export function registerSession(app: FastifyInstance, services: Services): void {
const { auth } = services.config;
// Per app instance rather than per module, so two servers in one process —
// which is what the tests are — cannot share a limiter.
const limiter = createLimiter(
auth.passwordLogin?.rateAttempts ?? 8,
auth.passwordLogin?.rateWindowSeconds ?? 300,
);
app.get("/api/v1/session", async (req) => {
const viewer = await services.auth.resolve(req);
return body(viewer.authenticated, viewer.subject, auth.passwordLogin !== null);
});
app.post("/api/v1/session", async (req, reply) => {
const login = auth.passwordLogin;
if (login === null) return reply.code(404).send(NOT_FOUND);
const retryAfter = limiter.check(req.ip);
if (retryAfter !== null) {
// Before reading the body and before hashing anything: the point of the
// limit is that a flood of attempts costs the box nothing.
return reply.code(429).header("retry-after", String(retryAfter)).send(RATE_LIMITED);
}
const supplied = credentials(req.body);
if (supplied === null) {
// A malformed body is a failed attempt, not a 400. Answering differently
// would let an attacker probe the endpoint without spending attempts.
limiter.fail(req.ip);
return reply.code(401).send(REJECTED);
}
const ok = await credentialsMatch(
login.username,
login.hash,
supplied.username,
supplied.password,
);
if (!ok) {
limiter.fail(req.ip);
return reply.code(401).send(REJECTED);
}
limiter.succeed(req.ip);
const token = issueSessionToken(auth, login.username, login.sessionTtlSeconds);
reply.header("set-cookie", sessionCookie(auth, token, login.sessionTtlSeconds));
return body(true, login.username, true);
});
// Signing out is available in every mode, including the ones where this box
// never issued the cookie. Refusing to clear a cookie tells the caller
// something about how the deployment is configured and helps nobody.
app.delete("/api/v1/session", async (_req, reply) => {
reply.header("set-cookie", clearedSessionCookie(auth));
return body(false, null, auth.passwordLogin !== null);
});
}
function body(authenticated: boolean, subject: string | null, passwordLogin: boolean): SessionBody {
return { authenticated, subject, passwordLogin };
}
/**
* Read the two fields, or decide there was nothing to read.
*
* Length caps are here rather than at the hash: a request body is up to a
* megabyte and there is no reason to hand any of it to a key-derivation
* function on an unauthenticated route.
*/
function credentials(raw: unknown): { username: string; password: string } | null {
if (raw === null || typeof raw !== "object") return null;
const { username, password } = raw as { username?: unknown; password?: unknown };
if (typeof username !== "string" || typeof password !== "string") return null;
if (username === "" || password === "") return null;
if (username.length > MAX_USERNAME_LENGTH || password.length > MAX_PASSWORD_LENGTH) return null;
return { username, password };
}
// ---- Rate limiting --------------------------------------------------------
interface Bucket {
failures: number;
windowEndsAt: number;
}
/**
* A fixed window of failures per client address, held in memory.
*
* In memory because there is one process and one box (CONTRACT.md §5) and a
* Redis to lose is worse than a limiter that resets on deploy. Keyed on
* `req.ip`, which is the real client because Caddy is the only thing that can
* reach the socket and `trustProxy` is on — nothing else can forge the header.
*
* Only failures count. A working session should not be able to lock its own
* owner out by reloading, and an attacker who is already succeeding is not
* someone a rate limit is going to help with.
*/
function createLimiter(attempts: number, windowSeconds: number) {
const buckets = new Map<string, Bucket>();
const limit = Math.max(1, Math.floor(attempts));
const windowMs = Math.max(1, Math.floor(windowSeconds)) * 1000;
/** Bounded so a flood from many addresses cannot grow the map without limit. */
const MAX_TRACKED = 4096;
function sweep(now: number): void {
for (const [key, bucket] of buckets) {
if (bucket.windowEndsAt <= now) buckets.delete(key);
}
// Still full after dropping the expired ones: that is a flood rather than
// accumulation, and clearing is the right trade. The worst case is that some
// attackers get their attempts back; the alternative is unbounded memory.
if (buckets.size >= MAX_TRACKED) buckets.clear();
}
return {
/** Seconds the caller should wait, or `null` when it may try now. */
check(ip: string): number | null {
const now = Date.now();
const bucket = buckets.get(ip);
if (bucket === undefined || bucket.windowEndsAt <= now) return null;
if (bucket.failures < limit) return null;
return Math.max(1, Math.ceil((bucket.windowEndsAt - now) / 1000));
},
fail(ip: string): void {
const now = Date.now();
if (buckets.size >= MAX_TRACKED) sweep(now);
const bucket = buckets.get(ip);
if (bucket === undefined || bucket.windowEndsAt <= now) {
buckets.set(ip, { failures: 1, windowEndsAt: now + windowMs });
return;
}
bucket.failures += 1;
},
succeed(ip: string): void {
buckets.delete(ip);
},
};
}