Make guessing a board's address hopeless rather than merely expensive, since the id is this app's entire access control.
What it is today.src/db.tsrandomId(8) over a 36-character alphabet: 36^8 = 2,821,109,907,456 addresses, 41.4 bits. Nothing throttles a miss — GET /b/<id> on an unknown id is a 15-byte 404 that can be asked for as fast as the network allows. The expected cost of finding some board scales down with how many boards exist: against 10,000 boards at 1,000 guesses a second, the first hit comes in about three days, and every hit is full read and write access to someone's board. The README is explicit that the link is the access control, so this number is the security of the app.
While in there: randomId maps a random byte with b % 36, and 256 is not a multiple of 36 — the first four letters come up 8/256 of the time against 7/256 for the rest. A small bias, but it is free to remove and it is the kind of thing that only ever gets worse in a copy-paste.
What done looks like
[ ] New board ids are 12 characters from the same alphabet (36^12 ≈ 4.7×10^18, ~62 bits) — the length cards and columns already use.
[ ] Existing 8-character boards keep working: every id check accepts 8 or 12 — src/routes.ts, src/mcp.ts, src/keys.ts (boardIdRe) — and a test opens a board inserted with an old-style id, through the page, through /mcp and through the terminal's open-by-id.
[ ] randomId draws without modulo bias (reject bytes at or above the largest multiple of the alphabet length and draw again), with a test that the distribution over a large sample is flat within tolerance.
[ ] A miss costs the caller something: board-not-found answers pass through a token bucket in src/limits.ts, in the style of boardCreationBucket, and past it answer 429 with retry-after instead of another 404. Only misses are counted — a person on a real board never touches it.
[ ] The README's Limits section names the new bucket and its number, and says plainly that it is an app-wide brake on scanning, not per-caller fairness (the same honest caveat boardCreationBucket carries).
[ ] The README's What's simulated section states the entropy: what a link is worth in bits, before and after.
[ ] bun test passes.
Files I expect to change:src/db.ts, src/routes.ts, src/mcp.ts, src/keys.ts, src/limits.ts, src/db.test.ts, src/server.test.ts, src/limits.test.ts, README.md.
Risks. Old links must not break — that is the one thing this change can get catastrophically wrong, hence the explicit test across all three seats. An app-wide bucket on misses means a scanner can turn everyone's 404 into a 429; that is a deliberate trade (a 429 with retry-after is a worse answer to a stale link than a 404, but a far worse one to a scanner) and belongs in the pull's own words. #57 is rewriting what a 404 looks like — whichever lands second should route the 429 through that same page rather than adding a second bare-text response.
Who fits:scout to build it, reviewed by sentinel — this is the app's access control and the review should check the id checks one by one.
Make guessing a board's address hopeless rather than merely expensive, since the id is this app's entire access control.
What it is today.
src/db.tsrandomId(8)over a 36-character alphabet: 36^8 = 2,821,109,907,456 addresses, 41.4 bits. Nothing throttles a miss —GET /b/<id>on an unknown id is a 15-byte 404 that can be asked for as fast as the network allows. The expected cost of finding some board scales down with how many boards exist: against 10,000 boards at 1,000 guesses a second, the first hit comes in about three days, and every hit is full read and write access to someone's board. The README is explicit that the link is the access control, so this number is the security of the app.While in there:
randomIdmaps a random byte withb % 36, and 256 is not a multiple of 36 — the first four letters come up 8/256 of the time against 7/256 for the rest. A small bias, but it is free to remove and it is the kind of thing that only ever gets worse in a copy-paste.What done looks like
src/routes.ts,src/mcp.ts,src/keys.ts(boardIdRe) — and a test opens a board inserted with an old-style id, through the page, through/mcpand through the terminal's open-by-id.randomIddraws without modulo bias (reject bytes at or above the largest multiple of the alphabet length and draw again), with a test that the distribution over a large sample is flat within tolerance.src/limits.ts, in the style ofboardCreationBucket, and past it answer429withretry-afterinstead of another 404. Only misses are counted — a person on a real board never touches it.boardCreationBucketcarries).bun testpasses.Files I expect to change:
src/db.ts,src/routes.ts,src/mcp.ts,src/keys.ts,src/limits.ts,src/db.test.ts,src/server.test.ts,src/limits.test.ts,README.md.Risks. Old links must not break — that is the one thing this change can get catastrophically wrong, hence the explicit test across all three seats. An app-wide bucket on misses means a scanner can turn everyone's 404 into a 429; that is a deliberate trade (a 429 with
retry-afteris a worse answer to a stale link than a 404, but a far worse one to a scanner) and belongs in the pull's own words. #57 is rewriting what a 404 looks like — whichever lands second should route the 429 through that same page rather than adding a second bare-text response.Who fits: scout to build it, reviewed by sentinel — this is the app's access control and the review should check the id checks one by one.