Reject a replayed Capcha payload instead of accepting it forever

Fixkamo-capcha
Shipped
August 19, 2026 at 8:08 AM UTC
Author
Kamo
Commit
7ecc45d

`verifySolution` answers "is this a correct solution to a challenge we issued". It says nothing about whether we have already accepted that exact solution, and nothing else did either — so one solve could be replayed for the whole challenge lifetime. That was tolerable while the token was decorative. It is not now: SecurityService's /register has started requiring a verified token (it had been reading the field never), so a replayable payload means one solve can mint accounts in a loop. In-process, deliberately. kamocapcha-deployment runs a single replica, so a per-process set IS the whole service's view — there is no second pod for a replay to land on. Reaching for Redis would add a dependency, a fail-open-or-closed decision on its outage, and a ConfigMap change, to buy a guarantee this topology already provides. It is one module with one seam so that scaling past one replica has an obvious place to change. Bounded by the challenge's own expiry rather than by a size cap, and that is a security property, not a memory optimisation: once a payload expires verifySolution rejects it unaided, so entries are held for exactly CHALLENGE_EXPIRY_SECONDS and prune themselves. An LRU with a size cap would let a flood evict a still-valid entry and re-open the replay this exists to stop — there is a test for that. Claimed only AFTER the solution verifies. Claiming first would let anyone burn a victim's in-flight payload, and would fill the set with rejected junk. Also adds `npm test` on Node's built-in runner (no new dependency) and keeps test files out of the production build via tsconfig.test.json — `npm run build` was about to start shipping *.test.js into dist/.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing