Skip to content

feat(components): frontend captcha gate — <zl-captcha> atom, patcher injection, mock verification - #562

Draft
fforootd wants to merge 1 commit into
mainfrom
frontend-captcha-gate
Draft

feat(components): frontend captcha gate — <zl-captcha> atom, patcher injection, mock verification#562
fforootd wants to merge 1 commit into
mainfrom
frontend-captcha-gate

Conversation

@fforootd

Copy link
Copy Markdown
Member

Summary

Implements the frontend half of ADR 019 (Captcha Gate Contract & Bot-Detection Signals), replacing stale #159 — that branch predates the June history rewrite (no common ancestor with main) and patches per-step Liquid templates that were since consolidated into default.liquid, so this is a fresh implementation on current main that ports its good parts.

  • New invisible <zl-captcha> atom (light DOM): reads the gate config from attributes, dispatches on kind + provider — built-in Altcha proof-of-work (Web Worker fast path, abortable main-thread fallback) or a third-party widget (Turnstile / hCaptcha / reCAPTCHA, mounted in light DOM because reCAPTCHA/hCaptcha break in shadow roots) — and emits the proof via zl-captcha-result.
  • Proofs are opaque strings per the existing flow-submit-request.yaml contract: base64-encoded JSON solution payload for Altcha (the standard Altcha wire format), the token for vendors. Zero OpenAPI/codegen changes — the spec's <zl-captcha> references and string-valued gate_proofs were already correct as written.
  • The mandatory-gates patcher now does what its docstring always claimed: any step.gates entry without a matching <zl-captcha> in the rendered template gets one injected (gates first, so solving starts on mount). default.liquid needs no gate markup at all — custom branding authors never have to know invisible atoms exist.
  • Orchestrator: collects proofs keyed by gate name, sends gate_proofs on submit, clears them per step, and surfaces error.gate_failed (new locale key in en/de/it) with the same anti-remount-loop guard as passkey errors. Submits are not blocked client-side; enforcement is the server's job (ADR 019 re-renders with a fresh challenge on failure).
  • Invisible atoms are uniformly null-safe: <zl-passkey> and <zl-captcha> auto-start from the first update cycle and also activate when options/config arrive late; mounting without data idles silently. Deliberate deviation from feat(components): <zl-gate> atom, null-safe invisible atoms, mock gate verification #159: an explicit startCeremony()/startSolve() without data still emits an error event (the suite pins that contract, and a loud failure on a consumer bug is correct).
  • api-mock: identifierStep now mints a real Altcha challenge for a bot_check gate on every render; setupMockHandlers({ verifyGates: true }) (the standalone dev server opts in) verifies submitted proofs and re-renders with error.gate_failed + a fresh challenge on a missing or tampered proof. Unit tests keep verification off by default.
  • Test infra: jsdom 29 ships crypto without subtle; the components unit project now backs it with Node's WebCrypto via vitest.setup.unit.ts.

Validation

  • moon run components:test — 366/366 (includes the end-to-end path: with verifyGates: true the patcher injects the atom, it solves the PoW, the submit carries gate_proofs, the mock verifies, and the flow reaches "You're signed in"; plus the anti-loop banner test)
  • moon run api-mock:test — 47/47 (mint/verify round-trip, missing-proof and tampered-proof rejection with fresh challenge, spec-conformance of the gated fixture)
  • moon run components:typecheck api-mock:typecheck — clean
  • moon run components:lint api-mock:lint — 0 errors, no new warnings
  • moon run components:build api-mock:build — clean
  • Browser (*.browser.spec.ts) projects not run locally; CI covers them.

Release notes / changeset

  • Changeset: .changeset/frontend-captcha-gate.md — minor for @zitadel/components (@zitadel/api-mock is changeset-ignored).

Notes

…, patcher injection, mock verification

Implements the frontend half of ADR 019 on current main, replacing stale
PR #159 (pre-rewrite history, dead template surface):

- New invisible light-DOM <zl-captcha> atom: Altcha proof-of-work (Web
  Worker fast path, abortable main-thread fallback) and Turnstile /
  hCaptcha / reCAPTCHA widgets; emits string proofs (base64 Altcha
  payload / vendor token) per the existing gate_proofs contract — no
  OpenAPI change.
- mandatory-gates patcher now injects <zl-captcha> for any step gate
  without a consumer (gates first, so solving starts on mount); the
  default template needs no gate markup.
- Orchestrator collects proofs, sends gate_proofs on submit, clears them
  per step, and surfaces error.gate_failed (en/de/it) with the same
  anti-remount-loop guard as passkey errors.
- Invisible atoms are uniformly null-safe: auto-start moves to the first
  update cycle and late-arriving options/config activate it; explicit
  start calls without data still error.
- api-mock mints a real Altcha challenge on the identifier step and,
  with verifyGates: true (standalone server), verifies proofs and
  re-renders with a fresh challenge on failure.
- jsdom 29 lacks crypto.subtle — unit project now backs it with Node's
  WebCrypto via vitest.setup.unit.ts.
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nextgen Ready Ready Preview, Comment Jul 20, 2026 2:04pm
nextgen-docs Ready Ready Preview, Comment Jul 20, 2026 2:04pm
nextgen-mock-zitadel Ready Ready Preview, Comment Jul 20, 2026 2:04pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 486a21b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@zitadel/components Minor
@zitadel/sdk-angular Minor
@zitadel/sdk-next Minor
@zitadel/sdk-nuxt Minor
@zitadel/sdk-qwik Minor
@zitadel/sdk-react Minor
@zitadel/sdk-solid Minor
@zitadel/sdk-svelte Minor
@zitadel/sdk-vue Minor
@zitadel/cli Minor
@zitadel/server Minor
@zitadel/server-linux-x64 Minor
@zitadel/server-linux-arm64 Minor
@zitadel/server-darwin-x64 Minor
@zitadel/server-darwin-arm64 Minor
@zitadel/server-win32-x64 Minor
@zitadel/api Minor
@zitadel/config Minor
@zitadel/sdk-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

1 participant