{"_id":"@bliztek/turnstile","name":"@bliztek/turnstile","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@bliztek/turnstile","version":"1.0.0","description":"Headless, zero-dependency Cloudflare Turnstile integration for React","type":"module","sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"build":"tsup","dev":"tsup --watch","prepublishOnly":"npm run build"},"keywords":["cloudflare","turnstile","captcha","react","headless"],"license":"MIT","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/bliztek/turnstile.git"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@types/react":"^19.0.0","react":"^19.0.0","tsup":"^8.0.0","typescript":"^5.0.0"},"gitHead":"ba7985772749c3c1ff84aa0e66617bd5d54c5ebb","_id":"@bliztek/turnstile@1.0.0","bugs":{"url":"https://github.com/bliztek/turnstile/issues"},"homepage":"https://github.com/bliztek/turnstile#readme","_nodeVersion":"25.6.1","_npmVersion":"11.9.0","dist":{"integrity":"sha512-NrVUkM7wEn/OIA1cOtqDVIK6a1o/z+X16a91LzoAhrYdbIJy0GhyXe9ZzJJYQYIoaSG0+VAuLqGaIFi/83ZHYQ==","shasum":"e8de724243d2d8d2773d1cdd2e513bb926c47fc8","tarball":"https://registry.npmjs.org/@bliztek/turnstile/-/turnstile-1.0.0.tgz","fileCount":13,"unpackedSize":27797,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDtbvh6s/HWgmIRmzgM0Fq+6VAs9GfiEvm8j8UGZH0tyAiBuX6Q/KXkyPbiXrz4sGXUEJKvSqkCA8JixudA4dzydDA=="}]},"_npmUser":{"name":"bliztek","email":"sbrown@bliztek.com"},"directories":{},"maintainers":[{"name":"bliztek","email":"sbrown@bliztek.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/turnstile_1.0.0_1773407059856_0.13141040113599511"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-13T13:04:19.789Z","1.0.0":"2026-03-13T13:04:20.001Z","modified":"2026-03-13T13:04:20.176Z"},"maintainers":[{"name":"bliztek","email":"sbrown@bliztek.com"}],"description":"Headless, zero-dependency Cloudflare Turnstile integration for React","homepage":"https://github.com/bliztek/turnstile#readme","keywords":["cloudflare","turnstile","captcha","react","headless"],"repository":{"type":"git","url":"git+https://github.com/bliztek/turnstile.git"},"bugs":{"url":"https://github.com/bliztek/turnstile/issues"},"license":"MIT","readme":"# @bliztek/turnstile\n\nHeadless, zero-dependency Cloudflare Turnstile integration for React.\n\n## Installation\n\n```bash\npnpm add @bliztek/turnstile\n```\n\nReact is an optional peer dependency — only required for the client-side hook. The server-side `verifyCaptcha` function works without React.\n\n## Entry Points\n\n| Import | What you get |\n|---|---|\n| `@bliztek/turnstile` | `useTurnstile` hook + client types |\n| `@bliztek/turnstile/server` | `verifyCaptcha` function + server types |\n\n## Client: `useTurnstile`\n\nA React hook that handles script loading, widget rendering, token state, and cleanup.\n\n```tsx\n\"use client\";\n\nimport { useTurnstile } from \"@bliztek/turnstile\";\n\nfunction ContactForm() {\n  const { ref, token, isReady, reset } = useTurnstile({\n    siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,\n    theme: \"dark\",\n  });\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!token) return;\n\n    const res = await fetch(\"/api/contact\", {\n      method: \"POST\",\n      body: JSON.stringify({ turnstileToken: token, /* ...form data */ }),\n    });\n\n    if (res.ok) {\n      reset(); // clears token and resets the widget\n    }\n  };\n\n  return (\n    <form onSubmit={handleSubmit}>\n      {/* form fields */}\n      <div ref={ref} className=\"min-h-[65px]\" />\n      <button type=\"submit\" disabled={!token}>Submit</button>\n    </form>\n  );\n}\n```\n\n### Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `siteKey` | `string` | required | Your Cloudflare Turnstile site key |\n| `theme` | `\"light\" \\| \"dark\" \\| \"auto\"` | `\"auto\"` | Widget appearance |\n| `size` | `\"normal\" \\| \"compact\"` | `\"normal\"` | Widget size |\n| `appearance` | `\"always\" \\| \"execute\" \\| \"interaction-only\"` | `\"always\"` | When to show the widget |\n| `retry` | `\"auto\" \\| \"never\"` | `\"auto\"` | Retry behavior on failure |\n| `retry-interval` | `number` | `8000` | Retry interval in ms |\n| `action` | `string` | — | Action identifier for analytics |\n| `cData` | `string` | — | Custom data passed to verification response |\n| `language` | `string` | — | BCP 47 language code (e.g. `\"en\"`, `\"de\"`) |\n| `tabindex` | `number` | — | Tab index for accessibility |\n| `onSuccess` | `(token: string) => void` | — | Called when a token is obtained |\n| `onExpire` | `() => void` | — | Called when the token expires |\n| `onError` | `(error: Error) => void` | — | Called when the script fails to load |\n\n### Return Value\n\n| Property | Type | Description |\n|---|---|---|\n| `ref` | `RefCallback<HTMLElement>` | Attach to the container element |\n| `token` | `string \\| null` | Current token, or null |\n| `isReady` | `boolean` | Whether the Turnstile script has loaded |\n| `reset` | `() => void` | Reset the widget and clear the token |\n\n### How It Works\n\n1. On mount, the hook loads the Turnstile script (`render=explicit` mode) via a singleton promise — multiple hook instances share one script tag.\n2. When the container element mounts (via ref callback) and the script is ready, the widget renders automatically.\n3. On successful challenge completion, `token` updates and `onSuccess` fires.\n4. On unmount, the widget is removed to prevent memory leaks.\n5. SSR-safe — all browser APIs are guarded behind `typeof window` checks.\n6. If the script fails to load, the singleton resets so future attempts can retry.\n\n## Server: `verifyCaptcha`\n\nVerifies a Turnstile token against the Cloudflare siteverify API. Uses only `fetch` — no SDK required.\n\n```ts\nimport { verifyCaptcha } from \"@bliztek/turnstile/server\";\n\nexport async function POST(request: Request) {\n  const { turnstileToken } = await request.json();\n\n  const result = await verifyCaptcha({\n    token: turnstileToken,\n    secretKey: process.env.TURNSTILE_SECRET_KEY!,\n    idempotencyKey: crypto.randomUUID(), // prevent replay attacks\n  });\n\n  if (!result.success) {\n    return Response.json(\n      { error: \"Verification failed\", details: result[\"error-codes\"] },\n      { status: 422 }\n    );\n  }\n\n  // Token is valid — proceed with the request\n}\n```\n\n### Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `token` | `string` | required | The token from the client widget |\n| `secretKey` | `string` | required | Your Cloudflare Turnstile secret key |\n| `endpoint` | `string` | Cloudflare URL | Override for testing |\n| `idempotencyKey` | `string` | — | Prevents replay attacks (recommended) |\n\n### Error Handling\n\n`verifyCaptcha` never throws. It always returns a `TurnstileVerificationResponse`:\n\n- Missing `token` or `secretKey` → `{ success: false, \"error-codes\": [\"missing-input-response\"] }`\n- HTTP error from Cloudflare → `{ success: false, \"error-codes\": [\"http-error-500\"] }`\n- Network failure → `{ success: false, \"error-codes\": [\"network-error\"] }`\n\n### Response\n\n```ts\ninterface TurnstileVerificationResponse {\n  success: boolean;\n  \"error-codes\"?: string[];\n  challenge_ts?: string;\n  hostname?: string;\n  action?: string;\n  cdata?: string;\n}\n```\n\n## Types\n\nClient types (from `@bliztek/turnstile`):\n- `UseTurnstileOptions` — All hook options\n- `UseTurnstileReturn` — Hook return value\n- `TurnstileRenderOptions` — Widget render options (subset of `UseTurnstileOptions`)\n- `TurnstileTheme`, `TurnstileSize`, `TurnstileAppearance`, `TurnstileRetry` — Union types\n- `TurnstileInstance`, `TurnstileWidgetOptions` — Low-level Cloudflare API types\n\nServer types (from `@bliztek/turnstile/server`):\n- `VerifyCaptchaOptions`\n- `TurnstileVerificationResponse`\n\n## Environment Variables\n\n| Variable | Side | Description |\n|---|---|---|\n| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | Client | Public site key (safe to expose) |\n| `TURNSTILE_SECRET_KEY` | Server | Secret key (never expose to client) |\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-b8f6fbc8722499304c4bb7c28401f5f1"}