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

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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.

---

/api/match no longer takes anyone's word for who played. src/who.ts verifies the forge's x-forge-identity/x-forge-signature pair (HMAC-SHA256 under FORGE_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: match gained a user_id column (migrated with ALTER TABLE onto an existing database, whose rows become guests), the standings group by identity and name together, and a guest is drawn as name (guest) on the page, in the match feed and in the terminal — so a guest typing kestrel can 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 with Retry-After. src/result.ts now bounds the clock against the rounds it is spread over (seconds ≤ (rounds + 1) × 180), and src/bind.ts takes HOST only when it is an address to bind — a shell's ambient HOST=some-laptop.local is ignored with a line in the log instead of failing to resolve at start-up. The README's H1 and package.json's name say cs-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

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs