Public: anyone can read this; the crew can change it.
One ration at the door, and a 429 that tells the truth: src/ration.ts in src/server.ts #49
Check · Review · Land · Live done — Landed and live in production.
The write door in `src/server.ts` now spends `take(key, now, PERSON_WRITES)` from `src/ration.ts`; `allow()`, `RATE` and `buckets` are deleted from `src/http.ts`. The key shape (`user:<id>`, `ip:<address>`) and the numbers (burst 20, 2/s) are untouched, so no policy changed — only who computes it, and now the answer includes the wait.
shipped production is serving it deploy 460b31c
Sign in to join this conversation.
The write door in
src/server.tsnow spendstake(key, now, PERSON_WRITES)fromsrc/ration.ts;allow(),RATEandbucketsare deleted fromsrc/http.ts. The key shape (user:<id>,ip:<address>) and the numbers (burst 20, 2/s) are untouched, so no policy changed — only who computes it, and now the answer includes the wait.A refusal carries
retry-afterin whole seconds, rounded up fromretryAfterMswith 1 as the floor on the wire.POST /api/*andPOST /mcpanswer{"error":"too_many_requests","message":…,"retry_after_ms":<exact>}; every other door keeps its plain-text sentence. Both carry the security floor —json()andtext()insrc/http.tsgrew an optional headers argument, the wayhtml()already had one.Worth a look:
servetakes an optionalnow()sosrc/server.test.tscan prove the wiring on a clock it controls — the 21st write in a burst is 429 withretry-after: 1, a request atretry_after_ms - 1is still refused, and one at exactlyretry_after_mssucceeds. Against a real server, a refusal 231ms from its token reportsretry_after_ms: 231andretry-after: 1.One correction to the issue's survey:
src/ration.test.tsalso importedallow(), in the parity test written against the old door. That test now assertsPERSON_WRITESand the twenty-then-two behaviour directly — the test was right about the numbers and only wrong about where they live.APP_EVENTSis untouched and left entirely to #8, as is the app half of the swap.bun test: 82 pass.🤖 Generated with Claude Code
Closes #40