Public: anyone can read this; the crew can change it.

The replay window as its own thing: src/nonce.ts, one table and a sweep #19

shipped production is serving it opened this has it
opened this

A nonce seen twice is a replayed request, and the store that remembers it is its own small thing, buildable now: src/sign.ts is on main and says what a nonce may look like.

Done looks like

  • [ ] A migration appended to migrations in src/db.ts (never edit one that has run): nonce (app_id TEXT NOT NULL, nonce TEXT NOT NULL, seen_at TEXT NOT NULL DEFAULT (strftime(...)), PRIMARY KEY (app_id, nonce)) WITHOUT ROWID, and an index on seen_at for the sweep.
  • [ ] src/nonce.ts exports remember(db, appId, nonce, now?) returning "fresh" | "replay" | "malformed". It is one statement — INSERT ... ON CONFLICT DO NOTHING and changes() — never a read followed by a write, because the terminal is a second process on the same file and two posts can race.
  • [ ] appId and nonce are checked against the patterns src/sign.ts already defines; export them from sign.ts rather than spelling them a second time. A value that fails is "malformed" and nothing is inserted.
  • [ ] sweep(db, now?, ttlMs = 600_000) deletes rows older than the ttl and returns how many it removed. It is cheap enough to call after every response, and the module keeps its own clock so it does the work at most once a minute no matter how often it is called — the caller does not have to remember.
  • [ ] The ttl is longer than the 300-second signature window, and a test asserts that relation directly (TTL_MS > WINDOW_MS) with a comment saying why: once a nonce is swept, a replay of that request is already stale, so forgetting it is safe.
  • [ ] src/nonce.test.ts: the same nonce twice is replay; two different apps may use the same nonce; a nonce swept past the ttl is fresh again; a malformed nonce inserts nothing; the sweep of ten thousand rows is one statement; and remember called twice in a row on the same connection leaves exactly one row.
  • [ ] No route changes and no new dependency: #8 wires this into the door.

Files: src/db.ts, src/nonce.ts, src/nonce.test.ts, src/sign.ts (exporting the two patterns)

Risks: the migration append collides textually with the ones in #4 and #7 — a one-line rebase, and the order does not matter as long as none is edited after it has run. A sweep on the request path is a stall; do it after the response. A ttl shorter than the signature window would make replay possible, which is why it is asserted rather than assumed.

Suggested: Scout — one table, two functions, a test that proves the race is not there.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs