{"_id":"@captigo/turnstile","_rev":"2-0af0be357be9257f475c8a7707f6e82f","name":"@captigo/turnstile","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@captigo/turnstile","version":"0.1.0","keywords":["captcha","turnstile","cloudflare","security","typescript","captigo"],"license":"MIT","_id":"@captigo/turnstile@0.1.0","maintainers":[{"name":"moritzmyrz","email":"moritzmyrz@gmail.com"}],"homepage":"https://github.com/moritzmyrz/captigo/tree/main/packages/turnstile#readme","bugs":{"url":"https://github.com/moritzmyrz/captigo/issues"},"dist":{"shasum":"80881c13c4bf879a7546e195aed65fa97171f94a","tarball":"https://registry.npmjs.org/@captigo/turnstile/-/turnstile-0.1.0.tgz","fileCount":14,"integrity":"sha512-VP6geiOB8lpJZDex8UtZhLjUHDruBapliyj4DOJXh5ZL/of5RUYms7GNISIES2TTvXmMYwpX4qV3SBHR8tj9ww==","signatures":[{"sig":"MEQCIEpTiaaOKR+lEwMIQpyYr9K5Lmtn9IKgIWxPZ+ZxbbZDAiB1GWfNzlhq0tn2+8heUfbLZj57QVjiR1NrI6W8A1zJbA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":61505},"main":"./dist/index.js","type":"module","_from":"file:captigo-turnstile-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsup","clean":"rm -rf dist tsconfig.tsbuildinfo","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"moritzmyrz","email":"moritzmyrz@gmail.com"},"_resolved":"/private/var/folders/lh/6742r5w95s9ddxh77dh_y6qw0000gn/T/a97495c308bab4353ccc7042c2a01c6f/captigo-turnstile-0.1.0.tgz","_integrity":"sha512-VP6geiOB8lpJZDex8UtZhLjUHDruBapliyj4DOJXh5ZL/of5RUYms7GNISIES2TTvXmMYwpX4qV3SBHR8tj9ww==","repository":{"url":"git+https://github.com/moritzmyrz/captigo.git","type":"git","directory":"packages/turnstile"},"_npmVersion":"10.9.2","description":"Cloudflare Turnstile adapter for captigo","directories":{},"sideEffects":false,"_nodeVersion":"22.13.0","dependencies":{"@captigo/core":"0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","jsdom":"^26.0.0","vitest":"^3.1.1","typescript":"^5.7.3","@captigo/shared":"0.0.0","@vitest/coverage-v8":"^3.1.1"},"_npmOperationalInternal":{"tmp":"tmp/turnstile_0.1.0_1775189006366_0.6205137864007202","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@captigo/turnstile","version":"0.2.0","description":"Cloudflare Turnstile adapter for Captigo — client widget and server-side verification","type":"module","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"main":"./dist/index.js","types":"./dist/index.d.ts","sideEffects":false,"dependencies":{"@captigo/core":"0.2.0"},"devDependencies":{"@vitest/coverage-v8":"^3.1.1","jsdom":"^26.0.0","tsup":"^8.3.0","typescript":"^5.7.3","vitest":"^3.1.1","@captigo/shared":"0.0.0"},"engines":{"node":">=20.0.0"},"keywords":["bot-management","captigo","captcha","cloudflare","security","turnstile","typescript"],"license":"MIT","homepage":"https://github.com/moritzmyrz/captigo/tree/main/packages/turnstile#readme","repository":{"type":"git","url":"git+https://github.com/moritzmyrz/captigo.git","directory":"packages/turnstile"},"bugs":{"url":"https://github.com/moritzmyrz/captigo/issues"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","clean":"rm -rf dist tsconfig.tsbuildinfo"},"_id":"@captigo/turnstile@0.2.0","_integrity":"sha512-DmiGO2PDMCb03s8v57uuTf8RyuR9CuHjQL9IcGxzUfr8JwyrFZZ/Wk5X2mxDhwXNlgBhsMXGKNFQdkLyY0pYug==","_resolved":"/private/var/folders/lh/6742r5w95s9ddxh77dh_y6qw0000gn/T/a287b381cc21ea5ca06883b87013705c/captigo-turnstile-0.2.0.tgz","_from":"file:captigo-turnstile-0.2.0.tgz","_nodeVersion":"22.13.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-DmiGO2PDMCb03s8v57uuTf8RyuR9CuHjQL9IcGxzUfr8JwyrFZZ/Wk5X2mxDhwXNlgBhsMXGKNFQdkLyY0pYug==","shasum":"7767781ffcdf9182b0e1950ae1038f17342f8118","tarball":"https://registry.npmjs.org/@captigo/turnstile/-/turnstile-0.2.0.tgz","fileCount":14,"unpackedSize":67603,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDB6JuCYk+S1HdWYlzonB8oNqETAjcY2XR1Txyt1snSlwIgGjRjke2Ho5d0zqBNqlfgJ318ZK5Kuu2K2I2pQq8C2Sg="}]},"_npmUser":{"name":"moritzmyrz","email":"moritzmyrz@gmail.com"},"directories":{},"maintainers":[{"name":"moritzmyrz","email":"moritzmyrz@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/turnstile_0.2.0_1775193168817_0.7826003462198277"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-03T04:03:26.238Z","modified":"2026-04-03T05:12:49.423Z","0.1.0":"2026-04-03T04:03:26.505Z","0.2.0":"2026-04-03T05:12:48.974Z"},"bugs":{"url":"https://github.com/moritzmyrz/captigo/issues"},"license":"MIT","homepage":"https://github.com/moritzmyrz/captigo/tree/main/packages/turnstile#readme","keywords":["bot-management","captigo","captcha","cloudflare","security","turnstile","typescript"],"repository":{"type":"git","url":"git+https://github.com/moritzmyrz/captigo.git","directory":"packages/turnstile"},"description":"Cloudflare Turnstile adapter for Captigo — client widget and server-side verification","maintainers":[{"name":"moritzmyrz","email":"moritzmyrz@gmail.com"}],"readme":"# @captigo/turnstile\n\n> Cloudflare Turnstile adapter for [Captigo](https://github.com/moritzmyrz/captigo) — client widget lifecycle and server-side token verification.\n\nProvides a browser-side widget lifecycle and a server-side token verification\nhelper — both behind the same `CaptchaAdapter` interface that the rest of the\ncaptigo ecosystem uses.\n\n---\n\n## Installation\n\n```bash\nnpm install @captigo/turnstile\n```\n\n`@captigo/core` is installed automatically as a transitive dependency. Add `@captigo/react` or `@captigo/vue` on the client if you use those integrations.\n\n---\n\n## Quick start\n\n### 1. Create the adapter\n\n```ts\nimport { turnstile } from \"@captigo/turnstile\";\n\nconst adapter = turnstile({\n  siteKey: \"0x4AAAAAAA...\", // your Turnstile site key\n});\n```\n\nPass the same `adapter` instance to both your client-side rendering code and\nyour server-side verification handler. The adapter holds no mutable state.\n\n---\n\n### 2. Client-side — render a widget\n\n```ts\nconst container = document.getElementById(\"captcha\")!;\n\nconst widget = adapter.render(container, {\n  callbacks: {\n    onSuccess: (token) => {\n      // token.value is the string to submit to your server\n      document.querySelector<HTMLInputElement>(\"[name=cf-turnstile-response]\")!.value =\n        token.value;\n    },\n    onExpire: () => {\n      // token expired — clear your stored value\n      console.log(\"Token expired, user will need to solve again.\");\n    },\n    onError: (err) => {\n      console.error(\"Turnstile error:\", err.message);\n    },\n  },\n});\n\n// On cleanup (e.g. component unmount):\nwidget.destroy();\n```\n\nThe Turnstile script is lazy-loaded the first time `render()` is called. You\ncan call `preloadScript()` earlier in your app to start that request sooner:\n\n```ts\nimport { preloadScript } from \"@captigo/turnstile\";\n\npreloadScript(); // fire and forget — safe to call multiple times\n```\n\n---\n\n### 3. Server-side — verify the token\n\n**This step is required.** Turnstile tokens are unverified on their own; you\nmust validate them against Cloudflare's API from your server before trusting\nthem.\n\nNever expose your secret key to the browser.\n\n```ts\n// In an API route, server action, or edge function:\nimport { adapter } from \"./captcha.js\"; // your shared adapter instance\n\nexport async function POST(request: Request) {\n  const body = await request.formData();\n  const token = body.get(\"cf-turnstile-response\") as string;\n\n  const result = await adapter.verify(token, process.env.TURNSTILE_SECRET!);\n\n  if (!result.success) {\n    return Response.json({ error: \"CAPTCHA verification failed\" }, { status: 400 });\n  }\n\n  // Proceed with the actual request\n  return Response.json({ ok: true });\n}\n```\n\nYou can also call the standalone `verifyToken()` function without creating an\nadapter — useful in edge runtimes or serverless functions where you don't want\nto import the browser-side widget code:\n\n```ts\nimport { verifyToken } from \"@captigo/turnstile\";\n\nconst result = await verifyToken(token, process.env.TURNSTILE_SECRET!);\n```\n\nThe optional third argument accepts `{ remoteip }` to forward the visitor's IP\nto Cloudflare for additional signal:\n\n```ts\nconst result = await verifyToken(token, secret, {\n  remoteip: request.headers.get(\"x-forwarded-for\") ?? undefined,\n});\n```\n\n#### Using the score\n\nTurnstile includes a bot-likelihood score in the verification response (0.0 =\nlikely bot, 1.0 = likely human). It is available on `result.score`:\n\n```ts\nconst result = await verifyToken(token, secret);\nif (!result.success) return Response.json({ error: \"CAPTCHA failed\" }, { status: 400 });\n\n// Optional: tighten the threshold beyond Cloudflare's own threshold.\nif ((result.score ?? 1) < 0.5) {\n  return Response.json({ error: \"Low confidence score\" }, { status: 400 });\n}\n```\n\n> **Note:** The score is only present for Managed and Invisible widgets. It may\n> be absent for some configurations — always treat it as optional.\n\n---\n\n## Invisible widget (interactive mode)\n\nTurnstile supports an invisible mode where no widget is rendered — the\nchallenge fires when you call `widget.execute()`. Set `execution: \"execute\"` to\nenable it:\n\n```ts\nconst adapter = turnstile({\n  siteKey: \"0x4AAAAAAA...\",\n  execution: \"execute\",\n});\n\n// adapter.meta.mode === \"interactive\"\n\nconst widget = adapter.render(container, { callbacks: { onSuccess: storeToken } });\n\n// On form submit:\nasync function handleSubmit() {\n  const token = await widget.execute(\"login\"); // action label for analytics\n  await submitFormWithToken(token.value);\n}\n```\n\nThe `execute()` call returns a `Promise<CaptchaToken>` that resolves when the\nchallenge completes (which may show a brief overlay to the user).\n\n---\n\n## Configuration reference\n\nAll options except `siteKey` are optional.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `siteKey` | `string` | — | **Required.** Your Turnstile site key. |\n| `execution` | `\"render\" \\| \"execute\"` | `\"render\"` | `\"render\"` = visible managed widget. `\"execute\"` = invisible, requires `widget.execute()`. |\n| `theme` | `\"light\" \\| \"dark\" \\| \"auto\"` | `\"auto\"` | Widget color scheme. |\n| `size` | `\"normal\" \\| \"compact\" \\| \"flexible\"` | `\"normal\"` | Widget dimensions. |\n| `language` | `string` | browser default | Language override (e.g. `\"en\"`, `\"de\"`). |\n| `appearance` | `\"always\" \\| \"execute\" \\| \"interaction-only\"` | `\"always\"` | When to show the widget UI. |\n| `action` | `string` | — | Label shown in the Turnstile analytics dashboard. Max 32 chars. |\n| `cData` | `string` | — | Arbitrary customer data attached to the challenge. Max 255 bytes. |\n| `retry` | `\"auto\" \\| \"never\"` | `\"auto\"` | Whether to auto-retry failed challenges. |\n| `retryInterval` | `number` | `8000` | Milliseconds between retries. |\n| `refreshExpired` | `\"auto\" \\| \"manual\" \\| \"never\"` | `\"auto\"` | Token refresh policy on expiry. |\n| `refreshTimeout` | `\"auto\" \\| \"manual\" \\| \"never\"` | `\"auto\"` | Behavior when the challenge times out. |\n| `tabindex` | `number` | — | Tab index for the widget iframe. |\n\n---\n\n## Widget API\n\n```ts\nconst widget = adapter.render(container, { callbacks });\n\nawait widget.execute(action?)  // trigger the challenge (interactive/managed)\nwidget.reset()                 // reset to unsolved state\nwidget.destroy()               // remove from DOM, release resources\nwidget.getToken()              // returns CaptchaToken | null\n```\n\n`execute()` behaviour depends on the adapter's mode:\n- **managed** (`execution: \"render\"`) — returns the current token if already\n  solved, otherwise waits for the next solve. The user drives the interaction.\n- **interactive** (`execution: \"execute\"`) — triggers the invisible challenge.\n  Resolves when the user completes it.\n\nCall `destroy()` on component unmount. After `destroy()`, do not call any other\nmethods on the widget instance.\n\n---\n\n## Error and expiry handling\n\n```ts\nimport { CaptchaError } from \"@captigo/turnstile\";\n\nconst widget = adapter.render(container, {\n  callbacks: {\n    onSuccess: (token) => {\n      submitForm(token.value);\n    },\n    onError: (err) => {\n      // err.code is one of: \"script-load-failed\" | \"provider-error\" | \"execute-failed\" | ...\n      console.error(`[${err.code}] ${err.message}`);\n      showErrorMessage(\"The CAPTCHA failed. Please try again.\");\n    },\n    onExpire: () => {\n      // Fired when a token expires OR when the challenge presentation times out.\n      // The widget auto-refreshes by default (refreshExpired / refreshTimeout: \"auto\").\n      clearStoredToken();\n    },\n  },\n});\n```\n\n**`onExpire` is called in two situations:**\n- A previously issued token has expired (the user took too long to submit).\n- The challenge timed out before the user completed it.\n\nIn both cases, any stored token is invalid and the widget will reset automatically\n(when using the default `refreshExpired: \"auto\"` / `refreshTimeout: \"auto\"` settings).\n\nSee the [CaptchaError source](https://github.com/moritzmyrz/captigo/blob/main/packages/core/src/errors.ts) for the full list of error codes.\n\n---\n\n## VerifyResult fields\n\n| Field | Type | Description |\n|---|---|---|\n| `success` | `boolean` | Whether the token passed verification. |\n| `provider` | `string` | Always `\"turnstile\"`. |\n| `challengeTs` | `string?` | ISO 8601 timestamp of challenge completion. |\n| `hostname` | `string?` | The hostname that rendered the widget. |\n| `score` | `number?` | Bot-likelihood score (0.0 = bot, 1.0 = human). |\n| `errorCodes` | `string[]?` | Cloudflare error codes if `success` is `false`. |\n\n---\n\n## Important notes\n\n- **Always verify server-side.** A token in your client is not proof of a\n  completed challenge until you validate it with `adapter.verify()` or\n  `verifyToken()`.\n- **One widget per container.** Rendering into the same container element twice\n  without calling `destroy()` first will cause unexpected behaviour.\n- **Secret key security.** `TURNSTILE_SECRET` must never be included in\n  client-side bundles. Keep it in environment variables only accessible to your\n  server.\n\n---\n\n## Documentation\n\n- [Server-side verification](https://github.com/moritzmyrz/captigo/blob/main/docs/server-verification.md) (Turnstile section)\n- [Compatibility / matrix](https://github.com/moritzmyrz/captigo/blob/main/docs/compatibility.md)\n- [Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/)\n\n[Repository](https://github.com/moritzmyrz/captigo) · [Issues](https://github.com/moritzmyrz/captigo/issues)\n","readmeFilename":"README.md"}