Repository: clerk/javascript
Pull request #5526: fix(clerk-js): Stop retrying on `/verify` if the client cannot solve the challenge
Author: login: anagstef
Labels: name: clerk-js
Created: 2025-04-03T18:20:36Z | Merged: 2025-04-08T13:39:12Z

## Description

This PR:

- Adds the `captcha_action` property on requests to the `/verify` endpoint in order to distinguish between `heartbeat` and `requires_captcha`
- Stops the client from keep retrying to solve the captcha if they are not able to solve it (due to unsupported browser or other reasons)
- Prevents the first `/verify` request on initialization if the client is captcha blocked, because components have not initialized yet.

## Checklist

- [x] `pnpm test` runs as expected.
- [x] `pnpm build` runs as expected.
- [ ] (If applicable) [JSDoc comments](https://jsdoc.app/about-getting-started.html) have been added or updated for any package exports
- [ ] (If applicable) [Documentation](https://github.com/clerk/clerk-docs) has been updated

## Type of change

- [x] 🐛 Bug fix
- [ ] 🌟 New feature
- [ ] 🔨 Breaking change
- [ ] 📖 Refactoring / dependency upgrade / documentation
- [ ] other:



Diff:
diff --git a/.changeset/fresh-apples-add.md b/.changeset/fresh-apples-add.md
new file mode 100644
index 00000000000..99246f19d9e
--- /dev/null
+++ b/.changeset/fresh-apples-add.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+Stop retrying on `/verify` if the client cannot solve the challenge
diff --git a/packages/clerk-js/src/core/fraudProtection.ts b/packages/clerk-js/src/core/fraudProtection.ts
index 1456c91ee11..f3f5d1a8d6c 100644
--- a/packages/clerk-js/src/core/fraudProtection.ts
+++ b/packages/clerk-js/src/core/fraudProtection.ts
@@ -1,12 +1,15 @@
 import { CaptchaChallenge } from '../utils/captcha/CaptchaChallenge';
 import type { Clerk } from './resources/internal';
-import { Client, isClerkAPIResponseError } from './resources/internal';
+import { ClerkRuntimeError, Client, isClerkAPIResponseError } from './resources/internal';
 
 export class FraudProtection {
   private static instance: FraudProtection;
 
   private inflightException: Promise<unknown> | null = null;
 
+  private captchaRetryCount = 0;
+  private readonly MAX_RETRY_ATTEMPTS = 3;
+
   public static getInstance(): FraudProtection {
     if (!FraudProtection.instance) {
       FraudProtection.instance = new FraudProtection(Client, CaptchaChallenge);
@@ -20,6 +23,13 @@ export class FraudProtection {
   ) {}
 
   public async execute<T extends () => Promise<any>, R = Awaited<ReturnType<T>>>(clerk: Clerk, cb: T): Promise<R> {
+    if (this.captchaAttemptsExceeded()) {
+      throw new ClerkRuntimeError(
+        'Security verification failed. Please try again by refreshing the page, clearing your browser cookies, or using a different web browser.',
+        { code: 'captcha_client_attempts_exceeded' },
+      );
+    }
+
     try {
       if (this.inflightException) {
         await this.inflightException;
@@ -47,10 +57,15 @@ export class FraudProtection {
       let resolve: any;
       this.inflightException = new Promise<unknown>(r => (resolve = r));
 
-      const captchaParams = await this.managedChallenge(clerk);
-
       try {
-        await this.client.getOrCreateInstance().sendCaptchaToken(captchaParams);
+        const captchaParams: any = await this.managedChallenge(clerk);
+        if (captchaParams?.captchaError !== 'modal_component_not_ready') {
+          await this.client.getOrCreateInstance().sendCaptchaToken(captchaParams);
+          this.captchaRetryCount = 0; // Reset the retry count on success
+        }
+      } catch (err) {
+        this.captchaRetryCount++;
+        throw err;
       } finally {
         // Resolve the exception placeholder promise so that other exceptions can be handled
         resolve();
@@ -64,4 +79,8 @@ export class FraudProtection {
   public managedChallenge(clerk: Clerk) {
     return new this.CaptchaChallengeImpl(clerk).managedInModal({ action: 'verify' });
   }
+
+  private captchaAttemptsExceeded = () => {
+    return this.captchaRetryCount >= this.MAX_RETRY_ATTEMPTS;
+  };
 }
diff --git a/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts b/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts
index 1455ca742e4..57459dc9aa8 100644
--- a/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts
+++ b/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts
@@ -15,7 +15,7 @@ export class CaptchaChallenge {
     const { captchaSiteKey, canUseCaptcha, captchaPublicKeyInvisible } = retrieveCaptchaInfo(this.clerk);
 
     if (canUseCaptcha && captchaSiteKey && captchaPublicKeyInvisible) {
-      return getCaptchaToken({
+      const captchaResult = await getCaptchaToken({
         siteKey: captchaPublicKeyInvisible,
         invisibleSiteKey: captchaPublicKeyInvisible,
         widgetType: 'invisible',
@@ -25,11 +25,12 @@ export class CaptchaChallenge {
         if (e.captchaError) {
           return { captchaError: e.captchaError };
         }
-        return { captchaError: e?.message || e };
+        return { captchaError: e?.message || e || 'unexpected_captcha_error' };
       });
+      return { ...captchaResult, captchaAction: opts?.action };
     }
 
-    return { captchaError: 'captcha_unavailable' };
+    return { captchaError: 'captcha_unavailable', captchaAction: opts?.action };
   }
 
   /**
@@ -45,7 +46,7 @@ export class CaptchaChallenge {
       retrieveCaptchaInfo(this.clerk);
 
     if (canUseCaptcha && captchaSiteKey && captchaPublicKeyInvisible) {
-      return getCaptchaToken({
+      const captchaResult = await getCaptchaToken({
         siteKey: captchaSiteKey,
         widgetType: captchaWidgetType,
         invisibleSiteKey: captchaPublicKeyInvisible,
@@ -55,11 +56,15 @@ export class CaptchaChallenge {
         if (e.captchaError) {
           return { captchaError: e.captchaError };
         }
-        return opts?.action === 'verify' ? { captchaError: e?.message || e } : undefined;
+        // if captcha action is signup, we return undefined, because we don't want to make the call to FAPI
+        return opts?.action === 'verify' ? { captchaError: e?.message || e || 'unexpected_captcha_error' } : undefined;
       });
+      return opts?.action === 'verify' ? { ...captchaResult, captchaAction: 'verify' } : captchaResult;
     }
 
-    return opts?.action === 'verify' ? { captchaError: 'captcha_unavailable' } : {};
+    // if captcha action is signup, we return an empty object, because it means that the bot protection is disabled
+    // and the user should be able to sign up without solving a captcha
+    return opts?.action === 'verify' ? { captchaError: 'captcha_unavailable', captchaAction: opts?.action } : {};
   }
 
   /**
diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts
index de2abf8a366..a1c4b675790 100644
--- a/packages/clerk-js/src/utils/captcha/turnstile.ts
+++ b/packages/clerk-js/src/utils/captcha/turnstile.ts
@@ -176,7 +176,15 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
     // but we won't show the modal as it will never escalate to interactive mode
     captchaWidgetType = widgetType;
     widgetContainerQuerySelector = modalContainerQuerySelector;
-    await openModal?.();
+    try {
+      await openModal?.();
+    } catch {
+      // When a client is captcha_block the first attempt to open the modal will fail with 'ClerkJS components are not ready yet.'
+      // This happens consistently in the first attempt, because in clerk.#loadInStandardBrowser we first await for the `/client` response
+      // and then we run initComponents to initialize the components.
+      // eslint-disable-next-line @typescript-eslint/only-throw-error
+      throw { captchaError: 'modal_component_not_ready' };
+    }
     const modalContainderEl = await waitForElement(modalContainerQuerySelector);
     if (modalContainderEl) {
       const { theme, language, size } = getCaptchaAttibutesFromElemenet(modalContainderEl);


Reviews:
user: login: anagstef
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T14:47:04Z
user: login: panteliselef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:47:53Z
user: login: anagstef
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T15:03:45Z
user: login: panteliselef
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:23:15Z
user: login: nikosdouvlis
body: as @anagstef raised, the logic here has become pretty complex and is due for a refactor. We'd also need to add some kind of UI for the C2 users explaining why the captcha failed
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T13:28:24Z

Review comments:
diff_hunk: @@ -47,10 +58,18 @@ export class FraudProtection {
       let resolve: any;
       this.inflightException = new Promise<unknown>(r => (resolve = r));
 
-      const captchaParams = await this.managedChallenge(clerk);
-
       try {
-        await this.client.getOrCreateInstance().sendCaptchaToken(captchaParams);
+        const captchaParams: any = await this.managedChallenge(clerk);
+        if (captchaParams?.captchaError !== 'modal_component_not_ready') {
+          await this.client.getOrCreateInstance().sendCaptchaToken(captchaParams);
+          this.captchaRetryCount = 0; // Reset the retry count on success
+        }
path: packages/clerk-js/src/core/fraudProtection.ts
user: login: anagstef
body: If it's the first `/client` request on initialization it will always fail because we always initialize the components after the first `/client` request.
created_at: 2025-04-07T14:47:03Z
author_association: MEMBER
diff_hunk: @@ -1,12 +1,16 @@
 import { CaptchaChallenge } from '../utils/captcha/CaptchaChallenge';
 import type { Clerk } from './resources/internal';
-import { Client, isClerkAPIResponseError } from './resources/internal';
+import { ClerkRuntimeError, Client, isClerkAPIResponseError } from './resources/internal';
 
 export class FraudProtection {
   private static instance: FraudProtection;
 
   private inflightException: Promise<unknown> | null = null;
 
+  private captchaRetryCount = 0;
+  private readonly MAX_RETRY_ATTEMPTS = 3;
+  private hasReachedDeadEnd = false;
path: packages/clerk-js/src/core/fraudProtection.ts
user: login: panteliselef
body: ```suggestion
  private captchaAttemptsExceeded = false;
```

but i'm curious why we need an additional variable to track this state.
created_at: 2025-04-07T14:47:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +24,13 @@ export class FraudProtection {
   ) {}
 
   public async execute<T extends () => Promise<any>, R = Awaited<ReturnType<T>>>(clerk: Clerk, cb: T): Promise<R> {
+    if (this.hasReachedDeadEnd) {
+      throw new ClerkRuntimeError(
+        'Security verification failed. Please try again by refreshing the page, clearing your browser cookies, or using a different web browser.',
+        { code: 'captcha_client_dead_end' },
path: packages/clerk-js/src/core/fraudProtection.ts
user: login: panteliselef
body: "dead end" seems a bit of a wild term.
```suggestion
        { code: 'captcha_client_attempts_exceeded' },
```
created_at: 2025-04-07T14:47:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,12 +1,16 @@
 import { CaptchaChallenge } from '../utils/captcha/CaptchaChallenge';
 import type { Clerk } from './resources/internal';
-import { Client, isClerkAPIResponseError } from './resources/internal';
+import { ClerkRuntimeError, Client, isClerkAPIResponseError } from './resources/internal';
 
 export class FraudProtection {
   private static instance: FraudProtection;
 
   private inflightException: Promise<unknown> | null = null;
 
+  private captchaRetryCount = 0;
+  private readonly MAX_RETRY_ATTEMPTS = 3;
+  private hasReachedDeadEnd = false;
path: packages/clerk-js/src/core/fraudProtection.ts
user: login: anagstef
body: You are right, removed. It's a leftover from extra code I had initially.
created_at: 2025-04-07T15:03:45Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-03T18:20:40Z
body: ###  🦋  Changeset detected

Latest commit: f0e8ab52b9abfc573b05edd437a4cc23ee10a31d

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

<details><summary>This PR includes changesets to release 3 packages</summary>

  | Name                    | Type  |
| ----------------------- | ----- |
| @clerk/clerk-js         | Patch |
| @clerk/chrome-extension | Patch |
| @clerk/clerk-expo       | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/clerk/javascript/new/stefanos/fraud-566-stop-retrying-if-verify-is-failing?filename=.changeset/metal-laws-matter.md&value=---%0A%22%40clerk%2Fclerk-js%22%3A%20patch%0A---%0A%0Afix(clerk-js)%3A%20Stop%20retrying%20on%20%60%2Fverify%60%20if%20the%20client%20cannot%20solve%20the%20challenge%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-03T18:20:40Z
body: [vc]: #NTiuGO7J4OXUhT49XzuaEqu0fUA4addKF8Go3RXXb8Y=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjbGVyay1qcy1zYW5kYm94IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImNsZXJrLWpzLXNhbmRib3gtZ2l0LXN0ZWZhbm9zLWZyYXVkLTU2Ni1zdG9wLXJldHJ5aW5nLWlmLThmYTk4Zi5jbGVya3N0YWdlLmRldiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vY2xlcmstcHJvZHVjdGlvbi9jbGVyay1qcy1zYW5kYm94LzJHQTh6TllTQWV2Z25rWlFnQ2JYcHJTWEFYcGoiLCJwcmV2aWV3VXJsIjoiY2xlcmstanMtc2FuZGJveC1naXQtc3RlZmFub3MtZnJhdWQtNTY2LXN0b3AtcmV0cnlpbmctaWYtOGZhOThmLmNsZXJrc3RhZ2UuZGV2IiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL2NsZXJrLWpzIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **clerk-js-sandbox** | ✅ Ready ([Inspect](https://vercel.com/clerk-production/clerk-js-sandbox/2GA8zNYSAevgnkZQgCbXprSXAXpj)) | [Visit Preview](https://clerk-js-sandbox-git-stefanos-fraud-566-stop-retrying-if-8fa98f.clerkstage.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/clerk-js-sandbox-git-stefanos-fraud-566-stop-retrying-if-8fa98f.clerkstage.dev?via=pr-comment-feedback-link) | Apr 8, 2025 11:32am |


author_association: NONE
user: login: anagstef
created_at: 2025-04-03T18:20:57Z
body: !snapshot
author_association: MEMBER
user: login: clerk-cookie
created_at: 2025-04-03T18:23:00Z
body: Hey @anagstef - the snapshot version command generated the following package versions:
| Package | Version |
| --- | --- |
| @clerk/agent-toolkit | 0.0.18-snapshot.v20250403182140 |
| @clerk/astro | 2.5.1-snapshot.v20250403182140 |
| @clerk/backend | 1.26.1-snapshot.v20250403182140 |
| @clerk/chrome-extension | 2.2.25-snapshot.v20250403182140 |
| @clerk/clerk-js | 5.59.1-snapshot.v20250403182140 |
| @clerk/elements | 0.23.10-snapshot.v20250403182140 |
| @clerk/clerk-expo | 2.9.8-snapshot.v20250403182140 |
| @clerk/expo-passkeys | 0.2.2-snapshot.v20250403182140 |
| @clerk/express | 1.4.1-snapshot.v20250403182140 |
| @clerk/fastify | 2.2.1-snapshot.v20250403182140 |
| @clerk/localizations | 3.13.6-snapshot.v20250403182140 |
| @clerk/nextjs | 6.13.1-snapshot.v20250403182140 |
| @clerk/nuxt | 1.5.1-snapshot.v20250403182140 |
| @clerk/clerk-react | 5.25.7-snapshot.v20250403182140 |
| @clerk/react-router | 1.2.1-snapshot.v20250403182140 |
| @clerk/remix | 4.5.13-snapshot.v20250403182140 |
| @clerk/shared | 3.3.1-snapshot.v20250403182140 |
| @clerk/tanstack-react-start | 0.13.1-snapshot.v20250403182140 |
| @clerk/testing | 1.4.35-snapshot.v20250403182140 |
| @clerk/themes | 2.2.28-snapshot.v20250403182140 |
| @clerk/types | 4.50.3-snapshot.v20250403182140 |
| @clerk/vue | 1.4.7-snapshot.v20250403182140 |

Tip: Use the snippet copy button below to quickly install the required packages.
`@clerk/agent-toolkit`
```sh
npm i @clerk/agent-toolkit@0.0.18-snapshot.v20250403182140 --save-exact
```
`@clerk/astro`
```sh
npm i @clerk/astro@2.5.1-snapshot.v20250403182140 --save-exact
```
`@clerk/backend`
```sh
npm i @clerk/backend@1.26.1-snapshot.v20250403182140 --save-exact
```
`@clerk/chrome-extension`
```sh
npm i @clerk/chrome-extension@2.2.25-snapshot.v20250403182140 --save-exact
```
`@clerk/clerk-js`
```sh
npm i @clerk/clerk-js@5.59.1-snapshot.v20250403182140 --save-exact
```
`@clerk/elements`
```sh
npm i @clerk/elements@0.23.10-snapshot.v20250403182140 --save-exact
```
`@clerk/clerk-expo`
```sh
npm i @clerk/clerk-expo@2.9.8-snapshot.v20250403182140 --save-exact
```
`@clerk/expo-passkeys`
```sh
npm i @clerk/expo-passkeys@0.2.2-snapshot.v20250403182140 --save-exact
```
`@clerk/express`
```sh
npm i @clerk/express@1.4.1-snapshot.v20250403182140 --save-exact
```
`@clerk/fastify`
```sh
npm i @clerk/fastify@2.2.1-snapshot.v20250403182140 --save-exact
```
`@clerk/localizations`
```sh
npm i @clerk/localizations@3.13.6-snapshot.v20250403182140 --save-exact
```
`@clerk/nextjs`
```sh
npm i @clerk/nextjs@6.13.1-snapshot.v20250403182140 --save-exact
```
`@clerk/nuxt`
```sh
npm i @clerk/nuxt@1.5.1-snapshot.v20250403182140 --save-exact
```
`@clerk/clerk-react`
```sh
npm i @clerk/clerk-react@5.25.7-snapshot.v20250403182140 --save-exact
```
`@clerk/react-router`
```sh
npm i @clerk/react-router@1.2.1-snapshot.v20250403182140 --save-exact
```
`@clerk/remix`
```sh
npm i @clerk/remix@4.5.13-snapshot.v20250403182140 --save-exact
```
`@clerk/shared`
```sh
npm i @clerk/shared@3.3.1-snapshot.v20250403182140 --save-exact
```
`@clerk/tanstack-react-start`
```sh
npm i @clerk/tanstack-react-start@0.13.1-snapshot.v20250403182140 --save-exact
```
`@clerk/testing`
```sh
npm i @clerk/testing@1.4.35-snapshot.v20250403182140 --save-exact
```
`@clerk/themes`
```sh
npm i @clerk/themes@2.2.28-snapshot.v20250403182140 --save-exact
```
`@clerk/types`
```sh
npm i @clerk/types@4.50.3-snapshot.v20250403182140 --save-exact
```
`@clerk/vue`
```sh
npm i @clerk/vue@1.4.7-snapshot.v20250403182140 --save-exact
```
author_association: COLLABORATOR
user: login: anagstef
created_at: 2025-04-07T08:32:36Z
body: !snapshot
author_association: MEMBER
user: login: clerk-cookie
created_at: 2025-04-07T08:34:36Z
body: Hey @anagstef - the snapshot version command generated the following package versions:
| Package | Version |
| --- | --- |
| @clerk/agent-toolkit | 0.0.18-snapshot.v20250407083314 |
| @clerk/astro | 2.6.0-snapshot.v20250407083314 |
| @clerk/backend | 1.27.0-snapshot.v20250407083314 |
| @clerk/chrome-extension | 2.2.25-snapshot.v20250407083314 |
| @clerk/clerk-js | 5.59.1-snapshot.v20250407083314 |
| @clerk/elements | 0.23.10-snapshot.v20250407083314 |
| @clerk/clerk-expo | 2.9.8-snapshot.v20250407083314 |
| @clerk/expo-passkeys | 0.2.2-snapshot.v20250407083314 |
| @clerk/express | 1.4.1-snapshot.v20250407083314 |
| @clerk/fastify | 2.2.1-snapshot.v20250407083314 |
| @clerk/localizations | 3.13.6-snapshot.v20250407083314 |
| @clerk/nextjs | 6.14.0-snapshot.v20250407083314 |
| @clerk/nuxt | 1.5.1-snapshot.v20250407083314 |
| @clerk/clerk-react | 5.26.0-snapshot.v20250407083314 |
| @clerk/react-router | 1.2.1-snapshot.v20250407083314 |
| @clerk/remix | 4.5.13-snapshot.v20250407083314 |
| @clerk/shared | 3.4.0-snapshot.v20250407083314 |
| @clerk/tanstack-react-start | 0.13.1-snapshot.v20250407083314 |
| @clerk/testing | 1.4.35-snapshot.v20250407083314 |
| @clerk/themes | 2.2.28-snapshot.v20250407083314 |
| @clerk/types | 4.51.0-snapshot.v20250407083314 |
| @clerk/vue | 1.5.0-snapshot.v20250407083314 |

Tip: Use the snippet copy button below to quickly install the required packages.
`@clerk/agent-toolkit`
```sh
npm i @clerk/agent-toolkit@0.0.18-snapshot.v20250407083314 --save-exact
```
`@clerk/astro`
```sh
npm i @clerk/astro@2.6.0-snapshot.v20250407083314 --save-exact
```
`@clerk/backend`
```sh
npm i @clerk/backend@1.27.0-snapshot.v20250407083314 --save-exact
```
`@clerk/chrome-extension`
```sh
npm i @clerk/chrome-extension@2.2.25-snapshot.v20250407083314 --save-exact
```
`@clerk/clerk-js`
```sh
npm i @clerk/clerk-js@5.59.1-snapshot.v20250407083314 --save-exact
```
`@clerk/elements`
```sh
npm i @clerk/elements@0.23.10-snapshot.v20250407083314 --save-exact
```
`@clerk/clerk-expo`
```sh
npm i @clerk/clerk-expo@2.9.8-snapshot.v20250407083314 --save-exact
```
`@clerk/expo-passkeys`
```sh
npm i @clerk/expo-passkeys@0.2.2-snapshot.v20250407083314 --save-exact
```
`@clerk/express`
```sh
npm i @clerk/express@1.4.1-snapshot.v20250407083314 --save-exact
```
`@clerk/fastify`
```sh
npm i @clerk/fastify@2.2.1-snapshot.v20250407083314 --save-exact
```
`@clerk/localizations`
```sh
npm i @clerk/localizations@3.13.6-snapshot.v20250407083314 --save-exact
```
`@clerk/nextjs`
```sh
npm i @clerk/nextjs@6.14.0-snapshot.v20250407083314 --save-exact
```
`@clerk/nuxt`
```sh
npm i @clerk/nuxt@1.5.1-snapshot.v20250407083314 --save-exact
```
`@clerk/clerk-react`
```sh
npm i @clerk/clerk-react@5.26.0-snapshot.v20250407083314 --save-exact
```
`@clerk/react-router`
```sh
npm i @clerk/react-router@1.2.1-snapshot.v20250407083314 --save-exact
```
`@clerk/remix`
```sh
npm i @clerk/remix@4.5.13-snapshot.v20250407083314 --save-exact
```
`@clerk/shared`
```sh
npm i @clerk/shared@3.4.0-snapshot.v20250407083314 --save-exact
```
`@clerk/tanstack-react-start`
```sh
npm i @clerk/tanstack-react-start@0.13.1-snapshot.v20250407083314 --save-exact
```
`@clerk/testing`
```sh
npm i @clerk/testing@1.4.35-snapshot.v20250407083314 --save-exact
```
`@clerk/themes`
```sh
npm i @clerk/themes@2.2.28-snapshot.v20250407083314 --save-exact
```
`@clerk/types`
```sh
npm i @clerk/types@4.51.0-snapshot.v20250407083314 --save-exact
```
`@clerk/vue`
```sh
npm i @clerk/vue@1.5.0-snapshot.v20250407083314 --save-exact
```
author_association: COLLABORATOR
user: login: anagstef
created_at: 2025-04-08T11:31:23Z
body: !snapshot
author_association: MEMBER
user: login: clerk-cookie
created_at: 2025-04-08T11:32:53Z
body: Hey @anagstef - the snapshot version command generated the following package versions:
| Package | Version |
| --- | --- |
| @clerk/chrome-extension | 2.2.26-snapshot.v20250408113211 |
| @clerk/clerk-js | 5.59.2-snapshot.v20250408113211 |
| @clerk/clerk-expo | 2.9.9-snapshot.v20250408113211 |

Tip: Use the snippet copy button below to quickly install the required packages.
`@clerk/chrome-extension`
```sh
npm i @clerk/chrome-extension@2.2.26-snapshot.v20250408113211 --save-exact
```
`@clerk/clerk-js`
```sh
npm i @clerk/clerk-js@5.59.2-snapshot.v20250408113211 --save-exact
```
`@clerk/clerk-expo`
```sh
npm i @clerk/clerk-expo@2.9.9-snapshot.v20250408113211 --save-exact
```
author_association: COLLABORATOR