{"_id":"@dxv-systems/turnstile","_rev":"5-9242bce839d20726819a5625b548c95d","name":"@dxv-systems/turnstile","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@dxv-systems/turnstile","version":"0.1.0","keywords":["dxv","turnstile","cloudflare","captcha","bot-protection"],"license":"MIT","_id":"@dxv-systems/turnstile@0.1.0","maintainers":[{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"}],"homepage":"https://github.com/dxv-systems/dxv-packages#readme","bugs":{"url":"https://github.com/dxv-systems/dxv-packages/issues"},"dist":{"shasum":"bea79dad59efdcc3acee6c8e6c072fece69eb4ab","tarball":"https://registry.npmjs.org/@dxv-systems/turnstile/-/turnstile-0.1.0.tgz","fileCount":18,"integrity":"sha512-/8i9c6CjEMZoFRtXh0QirEbdY5wKi7gFE5gQ0KYBZSmFMaQq0f/q8YqmoG8v3g5jr6iWQ6mw0LAbocXNTfDXdA==","signatures":[{"sig":"MEUCIQCl8NOsYoeEuXG8G6S5T08GHAx1dzMdVzmzd73F94AOkAIgWmnMJd/TjfgveWHVart7ur2f69NUJROqvcVbTWLzhjk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":35776},"main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"./express":{"types":"./dist/express.d.ts","default":"./dist/express.js"}},"gitHead":"8c7d3d520d745b39d7d1ec7d7d38d4a088376168","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"},"repository":{"url":"git+https://github.com/dxv-systems/dxv-packages.git","type":"git","directory":"packages/turnstile"},"_npmVersion":"11.12.1","description":"Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget","directories":{},"_nodeVersion":"24.15.0","_hasShrinkwrap":false,"devDependencies":{"react":"^18.3.1","vitest":"^2.1.8","express":"^4.21.2","supertest":"^7.0.0","typescript":"^5.6.0","@types/node":"^20.19.0","@types/react":"^18.3.12","@types/express":"^4.17.21","@types/supertest":"^6.0.2"},"peerDependencies":{"react":">=18","express":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/turnstile_0.1.0_1787149339071_0.19542553446375277","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@dxv-systems/turnstile","version":"0.1.1","keywords":["dxv","turnstile","cloudflare","captcha","bot-protection"],"license":"MIT","_id":"@dxv-systems/turnstile@0.1.1","maintainers":[{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"}],"homepage":"https://github.com/dxv-systems/dxv-packages#readme","bugs":{"url":"https://github.com/dxv-systems/dxv-packages/issues"},"dist":{"shasum":"12861cae71cebf7ce9c7ab5b101185197e6fadc2","tarball":"https://registry.npmjs.org/@dxv-systems/turnstile/-/turnstile-0.1.1.tgz","fileCount":22,"integrity":"sha512-fKaks8nI1L96tp0P+gbHJFUJ/TnDZ6q7KKuVJwUWrJlg7BZme5QfyKMadPw1RLRAeT8ANSRzEnGxl+YshmfmfQ==","signatures":[{"sig":"MEYCIQC2aDDIPbKDs3rLi8rZhhm7R8cibRaPt2uQZTVn8SO97QIhAOZ4ZxcN92hLALEac/GqRR70TDlD4B2TQ9XENFHmcTAv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37651},"main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"./express":{"types":"./dist/express.d.ts","default":"./dist/express.js"}},"gitHead":"d02620db9aaf7723e44a285c0b8adb4099086718","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0e252a27-f5b7-4302-af70-be84ae97c21d"}},"repository":{"url":"git+https://github.com/dxv-systems/dxv-packages.git","type":"git","directory":"packages/turnstile"},"_npmVersion":"11.5.1","description":"Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"react":"^18.3.1","vitest":"^2.1.8","express":"^4.21.2","supertest":"^7.0.0","typescript":"^5.6.0","@types/node":"^20.19.0","@types/react":"^18.3.12","@types/express":"^4.17.21","@types/supertest":"^6.0.2"},"peerDependencies":{"react":">=18","express":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/turnstile_0.1.1_1787150259269_0.769575557016539","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@dxv-systems/turnstile","version":"0.2.0","keywords":["dxv","turnstile","cloudflare","captcha","bot-protection"],"license":"MIT","_id":"@dxv-systems/turnstile@0.2.0","maintainers":[{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"}],"homepage":"https://github.com/dxv-systems/dxv-packages#readme","bugs":{"url":"https://github.com/dxv-systems/dxv-packages/issues"},"dist":{"shasum":"ac01a721b4ed5b11ae50820064662160c2283c79","tarball":"https://registry.npmjs.org/@dxv-systems/turnstile/-/turnstile-0.2.0.tgz","fileCount":22,"integrity":"sha512-ReGw76seCBrogioIDiU1IZRdBVFG+ype4z+fAaXt+RCTI4E5CXHdT9GhLB0VTQpcc+0WZiOBho2yhc9Txy/cNg==","signatures":[{"sig":"MEYCIQCRb5qn/HCURnKzTm6ECV3RHPrCb0nbDjfdVUAymNrTBwIhAI3z4cjQlb+9m/3afiGxkQq0pxbLpkDvNWsvuy5qmMUl","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":38757},"main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"./express":{"types":"./dist/express.d.ts","default":"./dist/express.js"}},"gitHead":"b223519758ac1d316eb0a873d5ee545e016c4878","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0e252a27-f5b7-4302-af70-be84ae97c21d"}},"repository":{"url":"git+https://github.com/dxv-systems/dxv-packages.git","type":"git","directory":"packages/turnstile"},"_npmVersion":"11.5.1","description":"Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"react":"^18.3.1","vitest":"^2.1.8","express":"^4.21.2","supertest":"^7.0.0","typescript":"^5.6.0","@types/node":"^20.19.0","@types/react":"^18.3.12","@types/express":"^4.17.21","@types/supertest":"^6.0.2"},"peerDependencies":{"react":">=18","express":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/turnstile_0.2.0_1787155801990_0.06597942574253612","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@dxv-systems/turnstile","version":"0.3.0","keywords":["dxv","turnstile","cloudflare","captcha","bot-protection"],"license":"MIT","_id":"@dxv-systems/turnstile@0.3.0","maintainers":[{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"}],"homepage":"https://github.com/dxv-systems/dxv-packages#readme","bugs":{"url":"https://github.com/dxv-systems/dxv-packages/issues"},"dist":{"shasum":"e89c74d6699a50cd02a3d21ff608376104f36929","tarball":"https://registry.npmjs.org/@dxv-systems/turnstile/-/turnstile-0.3.0.tgz","fileCount":22,"integrity":"sha512-HBti2P4JEjgf0P15fYDPk6ukcLRzcTodXTMUKJTmlLty9kOk2lKfJRn0N8EeNfQ/kloaa7rVyyWgivbMsuNQQQ==","signatures":[{"sig":"MEQCIBhWXMsjwHbhS3/g32J2nW9yOsg1depMmM1jjDHH3uySAiBZOGp0Cyns4rnZwsKQ59olxb6wF4ENyUqZor2NhY9p/Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":39975},"main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"./express":{"types":"./dist/express.d.ts","default":"./dist/express.js"}},"gitHead":"92a4b218e38a089e225307d3ca7cb23e203e9e55","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0e252a27-f5b7-4302-af70-be84ae97c21d"}},"repository":{"url":"git+https://github.com/dxv-systems/dxv-packages.git","type":"git","directory":"packages/turnstile"},"_npmVersion":"11.5.1","description":"Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"react":"^18.3.1","vitest":"^2.1.8","express":"^4.21.2","supertest":"^7.0.0","typescript":"^5.6.0","@types/node":"^20.19.0","@types/react":"^18.3.12","@types/express":"^4.17.21","@types/supertest":"^6.0.2"},"peerDependencies":{"react":">=18","express":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/turnstile_0.3.0_1787208836963_0.04206592971451251","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@dxv-systems/turnstile","version":"0.3.1","description":"Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget","keywords":["dxv","turnstile","cloudflare","captcha","bot-protection"],"engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./express":{"types":"./dist/express.d.ts","default":"./dist/express.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"build":"tsc","test":"vitest run","test:watch":"vitest","prepublishOnly":"npm run build"},"peerDependencies":{"express":">=4","react":">=18"},"peerDependenciesMeta":{"express":{"optional":true},"react":{"optional":true}},"devDependencies":{"@types/express":"^4.17.21","@types/node":"^20.19.0","@types/react":"^18.3.12","@types/supertest":"^6.0.2","express":"^4.21.2","react":"^18.3.1","supertest":"^7.0.0","typescript":"^5.6.0","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dxv-systems/dxv-packages.git","directory":"packages/turnstile"},"license":"MIT","_id":"@dxv-systems/turnstile@0.3.1","gitHead":"21f982b4ff49e94f3fe880cd927d603c97aaae67","bugs":{"url":"https://github.com/dxv-systems/dxv-packages/issues"},"homepage":"https://github.com/dxv-systems/dxv-packages#readme","_nodeVersion":"24.19.0","_npmVersion":"11.5.1","dist":{"integrity":"sha512-k8psb7/PXAt/SG3BAwC/C1zZulZUPphwl5ZP1JTHLXUgbAIZEd8h0OScQuVa4BFPxn7/dCbKK+u/ao1W8zn/fw==","shasum":"3f45f1faea9a430f38cca6dc5919164085094df7","tarball":"https://registry.npmjs.org/@dxv-systems/turnstile/-/turnstile-0.3.1.tgz","fileCount":22,"unpackedSize":40556,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD+EJLyGvXUgqwRX8uxdpnYP9/A6G6MiZINi8aCMtEI1AIgZgr9/sGVIDzYUuABMW05qeu987ZkVIUzzRMsj5JXQ2g="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0e252a27-f5b7-4302-af70-be84ae97c21d"}},"directories":{},"maintainers":[{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/turnstile_0.3.1_1787232763415_0.8702339114687352"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-19T14:22:18.836Z","modified":"2026-08-20T13:32:43.800Z","0.1.0":"2026-08-19T14:22:19.216Z","0.1.1":"2026-08-19T14:37:39.560Z","0.2.0":"2026-08-19T16:10:02.121Z","0.3.0":"2026-08-20T06:53:57.109Z","0.3.1":"2026-08-20T13:32:43.621Z"},"bugs":{"url":"https://github.com/dxv-systems/dxv-packages/issues"},"license":"MIT","homepage":"https://github.com/dxv-systems/dxv-packages#readme","keywords":["dxv","turnstile","cloudflare","captcha","bot-protection"],"repository":{"type":"git","url":"git+https://github.com/dxv-systems/dxv-packages.git","directory":"packages/turnstile"},"description":"Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget","maintainers":[{"name":"dxv-systems","email":"dunninkjesse1@gmail.com"}],"readme":"# @dxv-systems/turnstile\n\nCloudflare Turnstile for DXV apps: the `/siteverify` core, route guards for Express and Next.js App Router, and the React widget.\n\nExtracted from `dxv-platform`, where it guards the portal's unauthenticated auth routes.\n\n```bash\nnpm i @dxv-systems/turnstile\n```\n\n## Server\n\nBoth guards take the same two options and share one policy implementation, so they cannot drift apart.\n\n| Option | Meaning |\n|---|---|\n| `secret` | Usually `process.env.TURNSTILE_SECRET_KEY`. `undefined` is a legitimate state outside production. |\n| `failClosed` | What an **absent secret** means here. `true` → deny with 503 rather than serve unguarded. `false` → no secret, no guard. |\n\n**Express**\n\n```ts\nimport { requireTurnstile } from \"@dxv-systems/turnstile/express\";\n\napp.post(\"/api/login\", requireTurnstile({\n  secret: process.env.TURNSTILE_SECRET_KEY,\n  failClosed: isGuardedDeployment(),\n}), handler);\n```\n\n**Next.js App Router**\n\n```ts\nimport { assertTurnstile } from \"@dxv-systems/turnstile/next\";\n\nexport async function POST(req: NextRequest) {\n  const denied = await assertTurnstile(req, {\n    secret: process.env.TURNSTILE_SECRET_KEY,\n    failClosed: isGuardedDeployment(),\n  });\n  if (denied) return denied;\n\n  const body = Body.parse(await req.json());\n  // ...\n}\n```\n\n`assertTurnstile` takes a plain web `Request` and returns a plain `Response` — nothing here imports Next, so it works in any fetch-style handler. It never reads the body, so the handler's own parse still works.\n\n### What denies\n\nEvery axis fails closed, with one deliberate exception.\n\n| Situation | Result |\n|---|---|\n| No secret, `failClosed: false` | **allowed** — the explicit \"no guard here\" state |\n| No secret, `failClosed: true` | 503, and an error log |\n| No token on the request | 403, without calling `/siteverify` |\n| Challenge rejected | 403, codes logged |\n| `/siteverify` unreachable, times out, or 5xxs | 503 — an outage must not become a bypass |\n| Secret wrong (`invalid-input-secret` and friends) | 503, not 403 — this is our deploy being wrong, not the visitor being a bot |\n\n`failClosed` governs an **absent secret only**. Once a secret exists, a missing token and an unavailable check both deny regardless.\n\n### The client address\n\n`remoteip` is taken from **`x-real-ip`** only — the header Vercel's own `ipAddress()` helper reads. Never `x-forwarded-for`: a caller can send its own, and proxies append rather than replace, so the leftmost entry is caller-controlled.\n\nThere is deliberately no fallback. Cloudflare *scores the token against* `remoteip`, so a forged or wrong value poisons the scoring — worse than sending none, which is what happens when the header is absent.\n\n### Why no environment sniffing\n\nThis package never reads the environment. How an app recognises its own production deployment differs per host and per repo, and baking one answer in here would export it to every consumer. Callers pass `failClosed` and keep that decision where it belongs.\n\nA worked example of `isGuardedDeployment`, and the two traps behind it:\n\n```ts\n// NOT NODE_ENV: vercel.json commonly pins it to \"production\" for every target,\n// preview included. NOT VERCEL_ENV either: a Vercel *custom* environment\n// reports \"preview\", so staging is invisible to it — and staging usually does\n// have a real widget and secret, so it must fail closed too.\nexport function isGuardedDeployment(): boolean {\n  const env = process.env.VERCEL_TARGET_ENV;\n  return env === \"production\" || env === \"staging\";\n}\n```\n\n`VERCEL_TARGET_ENV` names custom environments and requires `expose_system_env_vars: true` on the project. Where it is unset — local dev, or a host that is not Vercel — the guard falls open, which is the right outcome for an environment with no widget provisioned.\n\n## Client\n\n```tsx\nimport { Turnstile, turnstileHeaders, type TurnstileHandle } from \"@dxv-systems/turnstile/react\";\n\nconst [token, setToken] = useState<string | null>(null);\nconst widget = useRef<TurnstileHandle>(null);\n\nasync function onSubmit() {\n  try {\n    await fetch(\"/api/login\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\", ...turnstileHeaders(token) },\n      body: JSON.stringify({ email, password }),\n    });\n  } finally {\n    widget.current?.reset(); // tokens are single-use\n  }\n}\n\n<Turnstile ref={widget} siteKey={SITE_KEY} failedMessage=\"Verification could not load.\" onToken={setToken} />\n<button type=\"submit\" disabled={!token}>Sign in</button>\n```\n\n`siteKey` and `failedMessage` are props, not read from the environment: the public-var prefix differs per bundler (`VITE_*`, `NEXT_PUBLIC_*`), and consuming apps disagree about whether they have an i18n runtime.\n\n### Theme\n\n`theme` defaults to `\"light\"`, deliberately **not** Cloudflare's own `\"auto\"`. `auto` follows the *operating system's* `prefers-color-scheme`, which has nothing to do with the host app's theme — so a light-only app renders a black widget for every visitor whose OS is in dark mode, which is what happened across the DXV fleet before 0.2.0.\n\nPass `\"auto\"` explicitly if your app genuinely follows the system scheme, or `\"dark\"` if it is dark-only. An app with its own theme toggle should pass its current theme, so the widget re-renders when it changes.\n\nWhere there is no real widget — local dev, preview deployments whose randomised hostnames can never match the domain allowlist — fall back to `TURNSTILE_TEST_SITE_KEY`, Cloudflare's always-passes key. That cannot weaken production: the sitekey is public, the server is the gate, and a token minted against it fails `/siteverify` against a real secret.\n\n### Wrap it in your app\n\nConsume `./react` through a thin app-owned component rather than importing it into pages directly:\n\n```tsx\n// components/turnstile.tsx\n\"use client\"; // Next only\n\nimport { Turnstile as Base, TURNSTILE_TEST_SITE_KEY, type TurnstileHandle } from \"@dxv-systems/turnstile/react\";\n\nexport const Turnstile = forwardRef<TurnstileHandle, { onToken: (t: string | null) => void }>(\n  function Turnstile(props, ref) {\n    return (\n      <Base\n        ref={ref}\n        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || TURNSTILE_TEST_SITE_KEY}\n        failedMessage={t(\"turnstile.failed\")}\n        {...props}\n      />\n    );\n  },\n);\n```\n\nThe wrapper is where the sitekey and the copy come from, and in Next it is also the `\"use client\"` boundary. The package's own `\"use client\"` directive survives the build but lands after the `\"use strict\"` that the CommonJS emit adds, so do not rely on it to make the boundary for you.\n\n## Development\n\n```bash\nnpm run build --workspace @dxv-systems/turnstile\nnpm run test  --workspace @dxv-systems/turnstile\n```\n\n`src/shared.ts` holds the pieces both halves need (`TURNSTILE_HEADER`, `TURNSTILE_TEST_SITE_KEY`, `turnstileHeaders`). Keep it that way: if `./react` imports them from `./index` instead, the whole server policy — `/siteverify`, the error-code table, `evaluateTurnstile` — lands in consumers' browser bundles, because CommonJS output defeats tree-shaking.\n\nOutput is CommonJS — Vercel's serverless runtime bundles to CJS, and an ESM-only dependency there is a runtime `ERR_REQUIRE_ESM`, not a build error.\n","readmeFilename":"README.md"}