Skip to content

refactor(types): name the captcha-type sets and group per-challenge session settings - #2967

Open
HughParry wants to merge 2 commits into
mainfrom
refactor/separate-image-puzzle-types
Open

refactor(types): name the captcha-type sets and group per-challenge session settings#2967
HughParry wants to merge 2 commits into
mainfrom
refactor/separate-image-puzzle-types

Conversation

@HughParry

Copy link
Copy Markdown
Contributor

Why

image | puzzle is 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, and pow | image | puzzle at 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, Session carries solvedImagesCount, powDifficulty and blocked as 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 — see sendCaptcha.

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) and InteractiveCaptchaType (image | puzzle) in @prosopo/types, with schemas, constant arrays and isChallengeCaptchaType / isInteractiveCaptchaType guards. Replaces every anonymous union. DecisionMachineCaptchaTypeSchema remains as an alias — stored decision-machine artefacts reference it.
  • One dispatch table per decision point: sendChallenge (provider) and VERIFY_RECENCY + the verifier record (@prosopo/server), both Record<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.
  • ClientSettingsSchema derives image / pow / puzzle groups on parse from the flat keys, which stay authoritative.

Behaviour change

One, deliberate: registerBlockedSession now takes the captcha type the request would have been served instead of hardcoding CaptchaType.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. Pure Block policies stay image (sanitizeAccessPolicy strips captchaType from 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 — sendCaptcha already discards it for pow and puzzle.

Testing

  • turbo run typecheck — 56/56 pass
  • turbo run test — provider 983/983, types 217/217 (integration suites need live Mongo/Redis and were not run locally)
  • New: captchaType.test.ts (guards + set membership, incl. a test that the challenge set covers every enum member except frictionless), 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)
  • Updated the registerBlockedSession assertions to pin the recorded type per access-policy captcha type

Deploy

Safe to ship alone and first. Provider must go out before any bundle that depends on it; nothing here requires a migration, because challengeParams and the settings groups are additive and dual-written.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-11T12:35:37Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 2967, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-13T12:13:07Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 2967, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-15T12:01:40Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 2967, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-16T18:01:45Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 2967, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-18T06:04:44Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 2967, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

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