refactor(types): name the captcha-type sets and group per-challenge session settings - #2967
refactor(types): name the captcha-type sets and group per-challenge session settings#2967HughParry wants to merge 2 commits into
Conversation
…ession settings `image | puzzle` was written out as an anonymous union at six places along the escalation path, and `pow | image | puzzle` at roughly ten more. Name both: `InteractiveCaptchaType` for the visual tier and `ChallengeCaptchaType` for "anything the user actually solves" (i.e. every type except the frictionless router). Adding a challenge type is now a compile error at each site instead of a grep exercise. `DecisionMachineCaptchaTypeSchema` stays as an alias because stored decision-machine artefacts reference it. Dispatch follows: the configured-type short-circuit, the access-policy handler and `@prosopo/server`'s verify path each had their own if/switch chain over the same three types. They now share exhaustive `Record<ChallengeCaptchaType, …>` tables (`sendChallenge`, `VERIFY_RECENCY`). The access-policy branches were identical apart from the image-only `solvedImagesCount`, which `sendCaptcha` already discards for other types, so passing it unconditionally is behaviour-preserving. Sessions additionally record `challengeParams`, a discriminated view of `solvedImagesCount` / `powDifficulty` / `blocked` keyed on the challenge type — those flat fields are meaningful for only one type each, and every writer had to remember to null the rest. It is dual-written alongside them and they remain the source of truth, so no reader changes and no backfill are needed here. `ClientSettingsSchema` likewise derives `image` / `pow` / `puzzle` groups on parse; they are optional on the output type so the many hand-built settings literals in tests and demos don't have to restate derived data. One behaviour change: `registerBlockedSession` takes the captcha type the request would have been served instead of hardcoding `image`. A blocked session from an access rule pinning pow or puzzle is now recorded against that type, which is what the same branch already logged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RJsacV3Y8wrzn8xexXPyeP
Conflict in the frictionless short-circuit: main extracted the bypass session params into `buildBypassSessionParams` and added the empty-pool PoW fallback; this branch replaced the per-type dispatch with `sendChallenge` and added the `isChallengeCaptchaType` guard. Kept main's structure and re-applied the guard and the dispatch table on top.
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
Why
image | puzzleis a real concept — the interactive/visual challenge tier that a PoW solve can escalate into — but it had no name. It was spelled out as an anonymous union at six places along the escalation path, andpow | image | puzzleat roughly ten more. Adding a challenge type meant finding all of them by hand.The same three types were then branched over independently in three places (the configured-type short-circuit, the access-policy handler, and
@prosopo/server's verify dispatch), each with its own if/switch chain.Separately,
SessioncarriessolvedImagesCount,powDifficultyandblockedas flat fields regardless of which challenge it served. Each is meaningful for exactly one type, and every writer has to remember to null the others — seesendCaptcha.This is phase 1 of separating image and puzzle. It is deliberately structural: no routing behaviour changes, so it can deploy on its own ahead of the phases that do.
What
ChallengeCaptchaType(pow | image | puzzle) andInteractiveCaptchaType(image | puzzle) in@prosopo/types, with schemas, constant arrays andisChallengeCaptchaType/isInteractiveCaptchaTypeguards. Replaces every anonymous union.DecisionMachineCaptchaTypeSchemaremains as an alias — stored decision-machine artefacts reference it.sendChallenge(provider) andVERIFY_RECENCY+ the verifier record (@prosopo/server), bothRecord<ChallengeCaptchaType, …>so a fourth type fails to compile rather than falling through.Session.challengeParams— a discriminated union keyed on challenge type, dual-written alongside the existing flat fields. Those stay the source of truth; no reader changes and no backfill are required by this PR.ClientSettingsSchemaderivesimage/pow/puzzlegroups on parse from the flat keys, which stay authoritative.Behaviour change
One, deliberate:
registerBlockedSessionnow takes the captcha type the request would have been served instead of hardcodingCaptchaType.image. A blocked session arising from an access rule that pins pow or puzzle is recorded against that type — which is already what that branch logs. PureBlockpolicies stayimage(sanitizeAccessPolicystripscaptchaTypefrom them, and no challenge is served).Everything else is behaviour-preserving. Where the per-type branches differed only by the image-only
solvedImagesCount, it is now passed unconditionally —sendCaptchaalready discards it for pow and puzzle.Testing
turbo run typecheck— 56/56 passturbo run test— provider 983/983, types 217/217 (integration suites need live Mongo/Redis and were not run locally)captchaType.test.ts(guards + set membership, incl. a test that the challenge set covers every enum member exceptfrictionless),challengeParams.test.ts(derivation per type, omission of absent knobs, schema round-trip),challengeDispatch.unit.test.ts(routing, single-dispatch, total coverage of the type set)registerBlockedSessionassertions to pin the recorded type per access-policy captcha typeDeploy
Safe to ship alone and first. Provider must go out before any bundle that depends on it; nothing here requires a migration, because
challengeParamsand the settings groups are additive and dual-written.