Skip to content

v0.7.0 — Proof of work + pending placeholder

Choose a tag to compare

@willybahuaud willybahuaud released this 10 Jun 20:26
· 2 commits to main since this release

What's new

Proof of work (opt-in)

Before this release, /captcha/init was free: a bot could harvest valid tokens and random field names at will, then forge a plausible behavioral log. The new PoW layer changes the economics — every token now costs CPU time first.

Enable it with pow => true in your Config. The init endpoint becomes two-phase on the same route: without a valid solution it returns an HMAC-signed challenge; with a valid solution it issues the token. Stateless, no extra storage required.

  • PoWChallengeGenerator — signed challenge (nonce + difficulty + expiry)
  • PoWVerifier — shape → signature → expiry → difficulty → replay (consumes nonces via the existing TokenStoreInterface when anti_replay is on)
  • pow_difficulty (default 18 bits, ~100–500 ms on desktop) and pow_challenge_ttl (default 90s) config options

Client-side solver

  • PoWSolver — inline SHA-256 inside a Blob-based Web Worker (no separate file to serve), with a chunked main-thread fallback when workers are blocked by CSP
  • AjaxFetcher handles the two-phase init transparently and re-solves a fresh challenge on every TTL refresh

Pending placeholder

The widget now appears immediately at page load as a dimmed, non-interactive placeholder — same dimensions as the final captcha (zero layout shift), and nothing scrapable (no field name, no token, no hidden inputs). It announces the verification step upfront and shows a spinner while the PoW resolves before upgrading in place.

Other changes

  • AbstractEndpoint::handleInit() now accepts the decoded JSON request body (required when pow is enabled, harmless otherwise)
  • Behavioral event collection starts at the first interaction signal — the trajectory during PoW solving is now captured
  • Token auto-refresh failures are caught instead of raising unhandled rejections

See the README for the configuration guide and the CHANGELOG for the full diff.