Public: anyone can read this; the crew can change it.
forked from trevato/cs · proposes · 4 behind
Security: the leaderboard is forgeable — inherited from cs unchanged #45
Check · Review · Land · Live done — Landed and live in production.
Pushed. Final state: `bun test` is 335 passing across 41 files, and the app boots and answers `/`, `/range` and a 404 in under 10 ms with the security floor intact.
shipped production is serving it deploy ae07c2d
Sign in to join this conversation.
new above
offline
Pushed. Final state:
bun testis 335 passing across 41 files, and the app boots and answers/,/rangeand a 404 in under 10 ms with the security floor intact.---
/api/matchno longer takes anyone's word for who played.src/who.tsverifies the forge'sx-forge-identity/x-forge-signaturepair (HMAC-SHA256 underFORGE_APP_IDENTITY_KEY, compared in constant time, expiry and shape checked, a name containing|kept whole); a result posted with a verified identity is recorded under that name whatever the name field said, and everyone else is a guest. Guests keep their own row:matchgained auser_idcolumn (migrated withALTER TABLEonto an existing database, whose rows become guests), the standings group by identity and name together, and a guest is drawn asname (guest)on the page, in the match feed and in the terminal — so a guest typingkestrelcan never land in signed-in kestrel's row. A match belongs to its identity, or for a guest to the session that started it; anyone else posting that id gets a 409.Writes are rate-limited by
src/limit.ts, a token bucket whose clock is a parameter: forty results back to back and one more every two seconds, keyed by identity for a signed-in player and by socket address plus session cookie for a guest. Guests are limited twice on purpose — the cookie is something the caller writes and could rotate, so the socket address carries a wider bucket underneath (200 burst, four a second), and signed-in players sit behind neither of those, so a guest flood cannot lock them out. Over either is a 429 withRetry-After.src/result.tsnow bounds the clock against the rounds it is spread over (seconds ≤ (rounds + 1) × 180), andsrc/bind.tstakesHOSTonly when it is an address to bind — a shell's ambientHOST=some-laptop.localis ignored with a line in the log instead of failing to resolve at start-up. The README's H1 andpackage.json's name saycs-engine, and a new "Who a score belongs to" section says how all of this is keyed and why.Worth looking at in the preview: the menu says whether you are a guest (with a link to the forge's sign-in,
rel="noreferrer") or signed in, and the leaderboard should show existing rows as guests, since nothing before this pull carried an identity. No new dependencies. What I did not do: the terminal still does not name who is at the keyboard, and the client bundle is untouched, so no ceiling moved.🤖 Generated with Claude Code
Closes #38