Skip to content

feat(js,shared): resolve Protect challenges automatically via a managed modal or inline marker - #9418

Open
mwickett wants to merge 1 commit into
mwickett/prot-855-extract-protect-check-runnerfrom
mwickett/prot-855-managed-protect-check
Open

feat(js,shared): resolve Protect challenges automatically via a managed modal or inline marker#9418
mwickett wants to merge 1 commit into
mwickett/prot-855-extract-protect-check-runnerfrom
mwickett/prot-855-managed-protect-check

Conversation

@mwickett

Copy link
Copy Markdown
Member

Description

Stacked on #9391. Second step toward fully Clerk-owned Protect challenge support for custom-flow (headless) apps: clerk-js now notices a challenge-gated response at the resource layer and resolves it itself, so the calling code's await simply completes with the post-challenge state — no customer challenge API, mirroring the bot-protection CAPTCHA contract.

  • ProtectCheckGate (core/protectCheckGate.ts): inspects successful payloads for a direct sign-in/sign-up response carrying a pending protect_check. When gated it acquires a host — the <div id="clerk-protect-check" /> placement marker when the page provides one, otherwise the managed modal — drives the shared challenge lifecycle from refactor(shared,ui): extract the Protect check lifecycle into shared internal helpers #9391 (executeProtectCheckWithTimeoutPATCH …/protect_checksubmitProtectCheckProof), loops chained challenges and expired-challenge reloads with caps, treats protect_check_already_resolved as soft success, and returns the post-challenge payload as the original call's result. One challenge session at a time: concurrent gated calls wait, then replay (the stored proof on the attempt lets replays pass).
  • FraudProtection.execute gains an optional raw-fetch argument and funnels every result path (including post-captcha replays) through the gate; BaseResource._fetch supplies the raw fetch so the gate's own PATCH/GET calls get full resource-call semantics (client piggyback updates, ClerkAPIResponseError on 4xx) without re-entering FraudProtection.
  • Clerk class: __internal_openProtectCheckModal / __internal_closeProtectCheckModal (same #clerkUI plumbing as the blank captcha modal) and __internal_registerProtectCheckHost(flow), which lets surfaces that render challenges themselves (prebuilt components, the upcoming inline marker component) suspend managed handling for their flow. All additive and __internal_-prefixed.
  • Modal reveal posture matches the captcha modal — opens invisible, non-dismissible, revealed by the script's setWidgetVisible handshake — plus a short delay reveal for long-running checks (proof-of-transfer) so a slow solve is never an invisible frozen page.
  • Fail-closed in no-RHC builds and non-DOM environments (protect_check_unsupported_environment).

Inert on its own: the ui package's ProtectCheckModal (next PR) provides the modal markup, and FAPI does not surface protect_check until the server-side version gates are raised. Rollout stays deploy-then-enable.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a3e2b4a

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

This PR includes changesets to release 23 packages
Name Type
@clerk/clerk-js Minor
@clerk/shared Patch
@clerk/chrome-extension Patch
@clerk/electron Patch
@clerk/expo Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/headless Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/ui Patch
@clerk/vue Patch
@clerk/swingset Patch

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

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 12, 2026 6:53pm
swingset Ready Ready Preview Aug 12, 2026 6:53pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3e2b4a21a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
}

const container = await waitForElement(`#${PROTECT_CHECK_MODAL_CONTAINER_ID}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Implement the modal before waiting for its container

When a custom-flow app has no clerk-protect-check marker, this waits forever: the reviewed packages/ui/src/Components.tsx has no protectCheckModal state, renderer, or ProtectCheckModal, and a repo-wide search finds no element creating cl-modal-protect-check-container. openModal('protectCheck') therefore only writes an unused dynamic state key, while waitForElement() has no timeout and never resolves null, leaving the original sign-in/sign-up promise permanently pending.

Useful? React with 👍 / 👎.

Comment on lines +148 to +149
if (!gated || this.hasRegisteredHost(gated.flow)) {
return current;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register prebuilt hosts before intercepting their responses

When a mounted prebuilt SignIn or SignUp submits a request that returns protect_check, hasRegisteredHost() is always false because the new __internal_registerProtectCheckHost API has no caller anywhere in the reviewed tree. Since interception happens inside BaseResource._fetch before the resource reaches the prebuilt router, the existing inline Protect card never receives the gated state; the request is diverted into managed handling instead (and currently hangs on the absent modal), contrary to the promised preserved inline experience.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant