Skip to content

feat(detector): serve detector only from per-session provider bundles; always-PoW fallback#2744

Open
HughParry wants to merge 5 commits into
mainfrom
feat/detector-bundle-pool-encryption
Open

feat(detector): serve detector only from per-session provider bundles; always-PoW fallback#2744
HughParry wants to merge 5 commits into
mainfrom
feat/detector-bundle-pool-encryption

Conversation

@HughParry

Copy link
Copy Markdown
Contributor

Part 1 of the per-session detector bundle delivery feature (provider/public side)

Adds the provider-side foundations for serving precomputed, per-session, individually-keyed detector bundles (the "bumblebee-style" pool), plus a graceful fallback. All changes are dark until the pool is initialised at boot (a follow-up), so existing frictionless detection is unaffected.

What's here

  • DetectorBundlePool (tasks/detection/bundlePool.ts) — Node analogue of bumblebee's bundle_manager: loads precomputed {id}.js/{id}.json bundle pairs from disk into memory, uniform-random per-session selection, hot-swap replace() for an admin push channel. Fails closed on an empty pool.
  • Redis short-TTL mapping (database/src/redisCache.ts) — cacheDetectorBundle/getDetectorBundle binding a detector session id → assigned bundle (cache:detector:{id}, default 60s), added to SESSION_KEY_PATTERNS.
  • Empty-pool → PoW fallback (getFrictionlessCaptchaChallenge/shortCircuit.ts) — when the pool is initialised but empty (no bundle can be assigned), serve a real PoW challenge instead of failing the request. When the pool is not initialised (feature off), the legacy detection path is used unchanged — so this PR is safe to land ahead of the boot wiring.

Tests

  • DetectorBundlePool: load/skip-malformed/uniform-coverage/hot-swap (8 tests).
  • PoW fallback decision: empty-pool → PoW, non-empty → proceed (2 tests).

Not in this PR (follow-ups)

  • Pool init at boot + assign/serve/admin-replace endpoints
  • decryptPayload resolution by bundle id (replacing the brute-force key loop)
  • Client externalisation + per-session bundle fetch

The companion encryption-layer + pool-build changes live in the private captcha-private repo PR.

🤖 Generated with Claude Code

@HughParry HughParry force-pushed the feat/detector-bundle-pool-encryption branch 3 times, most recently from 459dece to b716672 Compare June 22, 2026 14:42
…lback

Adds the provider-side building blocks for the precomputed, per-session
detector bundle pool (bumblebee-style):

- DetectorBundlePool: load precomputed {id}.js/{id}.json pairs into memory,
  uniform-random per-session pick, hot-swap replace() for admin push.
- Redis short-TTL session->bundle mapping (cache:detector:{id}, 60s).
- Frictionless fallback to a real PoW challenge when the pool is initialised
  but empty. Dark until the pool is initialised at boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HughParry HughParry force-pushed the feat/detector-bundle-pool-encryption branch 2 times, most recently from 525e857 to 85483e7 Compare June 23, 2026 10:30
…resolution

Wires the detector bundle pool end-to-end (Part 2):

- Boot: initialise the pool from PROSOPO_DETECTOR_POOL_DIR ONLY when the dir
  exists (absent => legacy path; present-but-empty => PoW fallback).
- Client assign endpoint (POST .../client/detector/assign): picks a random
  bundle, stores the short-TTL session->bundle binding, returns the obfuscated
  detector inline; returns useProviderBundle:false when no pool. Excluded from
  the Prosopo-User header requirement (runs before the account exists).
- Admin replace-pool endpoint (hot-swap the in-memory pool).
- decryptPayload resolves the assigned bundle (single deterministic decrypt with
  its own keypair + inner cipher config) and falls back to the legacy key pool.
  innerConfig threaded through getBotScore + regenerated decode bundles.
- Client (customDetectBot): resolves provider, asks for a bundle, loads it via
  blob import or falls back to the bundled detector; threads detectorSessionId.
- types: ApiParams.detectorSessionId, assign/replace paths + bodies.

All dark unless a pool dir is provisioned, so existing detection is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HughParry HughParry force-pushed the feat/detector-bundle-pool-encryption branch from 85483e7 to 41bd528 Compare June 23, 2026 11:14
@HughParry HughParry marked this pull request as draft June 23, 2026 11:41
…; always-PoW fallback

The detector now lives ONLY in the provider-served pool bundles — no bundled/
inlined detector and no legacy detector-key pool. Each session's bundle encrypts
everything it produces (score, SIMD readings, behavioural data) with its own RSA
keypair + inner ChaCha20-Poly1305 cipher; the provider decrypts each payload with
that exact bundle.

- Pool is always initialised at boot (missing/empty dir ⇒ empty pool), so the
  three states collapse to two: bundles present ⇒ per-session serving; no bundles
  ⇒ always PoW (covers no-pool, empty-pool, and client `detectorUnavailable`).
- `detectorSessionId → bundleId` Redis binding resolved at the frictionless hop;
  the bundleId is promoted onto the durable session record so later hops (SIMD
  attach, PoW/puzzle/image solution submit) decrypt with the same bundle.
- Client: removed the inlined `@prosopo/detector` runtime import (type-only now);
  when no provider bundle can be obtained/run it signals `detectorUnavailable`
  and the provider serves PoW.
- All server decrypt paths resolve the session bundle and pass its inner cipher;
  legacy key-pool brute force + env fallback removed. Decrypt failure fails closed.
- Session record + schema gain `bundleId`; frictionless request gains
  `detectorUnavailable`. Tests updated for the bundle-only model.

Note: temporary [POOL-DEBUG] logging retained for local validation; strip before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HughParry HughParry changed the title feat(provider): detector bundle pool foundations + empty-pool PoW fallback feat(detector): serve detector only from per-session provider bundles; always-PoW fallback Jun 24, 2026
@HughParry HughParry marked this pull request as ready for review June 24, 2026 12:09
HughParry and others added 2 commits June 24, 2026 13:53
… detector dep

Fixes two CI failures from the detector-only-on-providers change:

- e2e: the no-detector PoW fallback built session params from the client's
  (empty) token, and sendCaptcha rejects a falsy token ("Session parameters
  must be set before sending a pow captcha") → 400 → widget stuck. The bypass
  session legitimately has no detection token, so synthesise a unique one.
- lint:refs: `@prosopo/detector` is now a type-only import in
  procaptcha-frictionless, so it must be a tsconfig reference, not a runtime
  dependency. Removed it from package.json dependencies and the tsconfig
  references (type still resolves via the workspace); lockfile updated.

Adds a regression test asserting the no-detector fallback never passes an empty
token to sendPowCaptcha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sopo/detector

The previous change removed @prosopo/detector from procaptcha-frictionless's deps
and tsconfig references (it's now type-only) — but `typeof import("@prosopo/detector")`
still needs the package's .d.ts, so `tsc --build` failed in CI ("Cannot find module
'@prosopo/detector'") because detector is no longer built as a project reference.

Declare the detector's default-export signature locally from shared @prosopo/types
primitives instead. This satisfies both the ref-linter (no detector dep/ref) and
`tsc --build` (no detector build needed), with no behavioural change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant