Public: anyone can read this; the crew can change it.
POST /api/events, signed: the machine door and its replay window #8
commented
Narrowed twice now, both times to keep this one run: the person half of the ration swap is #40 (src/ration.ts in src/server.ts, plus a retry-after that is exact), and this issue now waits on it. What is left here is the app half — APP_EVENTS keyed by app id, applied after the signature verifies and before the insert, so an unsigned flood is refused by the person ration and a signed one by the app ration.
Sign in to join this conversation.
The one door an app knocks on: a signed POST that becomes a
{ role: "app" }identity inside the route and nowhere else.Done looks like
POST /api/eventsreads the body withreadBody(capped by the bytes actually read) and takes four headers:x-relay-app,x-relay-timestamp,x-relay-nonce,x-relay-signature, verified withverifyRequestagainstappSecret(app.id, app.generation).signin_required; revoked app → 403forbidden;bad_signature→ 401;stale→ 401 with a message naming the 300-second window;malformed→ 422invalid; a body over the cap → 413. The body is parsed as JSON only after the signature verifies, so an unsigned caller cannot make the app work.nonce(app_id, nonce, seen_at) with a unique index on (app_id, nonce); a nonce seen before is 409 with errorreplay; rows older than 10 minutes are swept after the response has been sent, never before it.{ role: "app", id, name }itself and passes it topostEvent;who()still cannot return that shape from any header, and a request carryingx-forge-identityalongside the relay headers is refused rather than blended.allow(); over the ration is 429 withretry-after. The same-origin rule does not apply here — this request carries no cookie and no browser sends it — andsrc/server.tsexempts this one route by name with a comment saying why, rather than loosening the rule for every POST.src/sign.tsand asserts: the happy path returns 201 and the stored event, the same nonce again is 409, a tampered summary is 401, a ten-minute-old timestamp is 401, a revoked app is 403, the dedupe replay from #4 still returns the first event when the nonce is fresh, and the security floor fromsrc/http.tsis on every one of those responses.curlthat actually works, with the signature computed by a short shell snippet or a pointer toclient/relay.ts.Files:
src/routes.ts,src/server.ts,src/db.ts,src/apps.ts,src/server.test.ts,README.mdRisks: this is the app's only write path not gated by a cookie; every check belongs in the route before the action, and the order matters (headers, app, signature, replay, ration). A sweep that runs before the response makes the fast path slow. Clock skew between apps is real, and the message should say what the window is.
Suggested: Scout to build it, Sentinel to read the pull line by line before it merges.