feat(js,shared): resolve Protect challenges automatically via a managed modal or inline marker - #9418
Conversation
🦋 Changeset detectedLatest commit: 7cea771 The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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}`); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (!gated || this.hasRegisteredHost(gated.flow)) { | ||
| return current; |
There was a problem hiding this comment.
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 👍 / 👎.
a3e2b4a to
8a43022
Compare
c740264 to
b1f3415
Compare
8a43022 to
3885954
Compare
…ed modal or inline marker
b1f3415 to
612ee16
Compare
3885954 to
7cea771
Compare
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
awaitsimply completes with the post-challenge state — no customer challenge API, mirroring the bot-protection CAPTCHA contract. Revised after an external review round; the semantics below reflect the revision.ProtectCheckGate(core/protectCheckGate.ts): inspects successful payloads for a direct sign-in/sign-up response carrying a pendingprotect_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 (executeProtectCheckWithTimeout→PATCH …/protect_check→submitProtectCheckProof), loops chained challenges and expired-challenge reloads with caps, treatsprotect_check_already_resolvedas soft success, and then replays the original operation — the PATCH only clears the gate; for pre-op gates (e.g. a gatedprepareFirstFactor) the gated side effect only happens on the replay, which passes because the attempt's stored proof is re-presented server-side. The caller receives the replay's result. One challenge session at a time: concurrent gated calls wait, then replay._baseFetchskips the client piggyback update when the direct response is challenge-gated, so listeners (state/signals, older framework SDKs) never observe the intermediateneeds_protect_checkwhile the promise is held. When a registered host owns the gate (prebuilt components), the gate publishes the pending state itself; every replay/PATCH publishes its own final payload as usual.openModalresolving without the container ever appearing (an older hot-loaded@clerk/uiaccepting the unknown modal name) fails within 5s instead of hanging; session release awaits the modal close so back-to-back sessions can't race.AbortSignalis threaded through the gate — challenge execution, proof submission, reloads, and queued replays all observe it, and the host is released on abort.FraudProtection.executegains an optional per-request protect context (raw fetch + deferred-publish + signal) supplied byBaseResource._fetch; every result path, including post-captcha replays, funnels through the gate.__internal_openProtectCheckModal/__internal_closeProtectCheckModal(same#clerkUIplumbing as the blank captcha modal) and__internal_registerProtectCheckHost(flow)for surfaces that render challenges themselves (the prebuilt components; the placement marker is deliberately not a registrant). All additive and__internal_-prefixed.setWidgetVisiblehandshake — plus a short delay reveal for long-running checks (proof-of-transfer) so a slow solve is never an invisible frozen page.protect_check_unsupported_environment).Inert on its own: the ui package's
ProtectCheckModal(next PR) provides the modal markup, and FAPI does not surfaceprotect_checkuntil the server-side version gates are raised. Rollout stays deploy-then-enable; the user-facing changelog entry ships with the PR that makes the feature functional.Known follow-ups tracked separately: OAuth redirect/popup completion resolves the gate from the nested client mirror (PROT-968); no-RHC/version-skew enablement caveats and PR3/PR4 feature detection (PROT-969); challenge
sdk_urlorigin-restriction decision (PROT-970); expired-challenge re-mint contract + clock-skew allowance (PROT-946).Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change