Skip to main content 首页 创作者 ovachiever droid-tings cloudflare-turnstile
cloudflare-turnstile Add bot protection with Turnstile (CAPTCHA alternative). Use when: protecting forms, securing login/signup, preventing spam, migrating from reCAPTCHA, integrating with React/Next.js/Hono, implementing E2E tests, or debugging CSP errors, token validation failures, or error codes 100*/300*/600*.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/ovachiever/droid-tings --skill cloudflare-turnstile命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... Build backend AI with Vercel AI SDK v5/v6. Covers v6 beta (Agent abstraction, tool approval, reranking),
v4→v5 migration (breaking changes), latest models (GPT-5/5.1, Claude 4.x, Gemini 2.5), Workers startup
fix, and 12 error solutions (AI_APICallError, AI_NoObjectGeneratedError, streamText silent errors).
Use when: implementing AI SDK v5/v6, migrating v4→v5, troubleshooting errors, fixing Workers startup
issues, or updating to latest models.
Build React chat interfaces with Vercel AI SDK v5/v6. Covers v6 beta (agent integration, tool approval,
auto-submit), v4→v5 migration (breaking changes), useChat/useCompletion/useObject/useAssistant hooks,
and 12 UI error solutions (stream parsing, stale body values, React update depth).
Use when: implementing AI SDK v5/v6 chat UIs, migrating v4→v5, troubleshooting "useChat failed to parse
stream", "useChat no response", or "stale body values" errors, or integrating OpenAI assistants.
name cloudflare-turnstile description Add bot protection with Turnstile (CAPTCHA alternative). Use when: protecting forms, securing login/signup, preventing spam, migrating from reCAPTCHA, integrating with React/Next.js/Hono, implementing E2E tests, or debugging CSP errors, token validation failures, or error codes 100*/300*/600*.
license MIT
Cloudflare Turnstile
Status : Production Ready
Last Updated : 2025-10-22
Dependencies : None (optional: @marsidev/react-turnstile for React)
Latest Versions : @marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3
Quick Start (10 Minutes)
1. Create Turnstile Widget
Get your sitekey and secret key from Cloudflare Dashboard.
Why this matters:
Each widget has unique sitekey/secret pair
Sitekey goes in frontend (public)
Secret key ONLY in backend (private)
Use different widgets for dev/staging/production
2. Add Widget to Frontend
Embed the Turnstile widget in your HTML form.
<!DOCTYPE html >
<html >
<head >
< = >
Submit
script
src
"https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
</script >
</head >
<body >
<form id ="myForm" action ="/submit" method ="POST" >
<input type ="email" name ="email" required >
<div class ="cf-turnstile" data-sitekey ="YOUR_SITE_KEY" >
</div >
<button type ="submit" >
</button >
</form >
</body >
</html >
Never proxy or cache api.js - must load from Cloudflare CDN
Widget auto-creates hidden input cf-turnstile-response with token
Token expires in 5 minutes
Each token is single-use only
3. Validate Token on Server ALWAYS validate the token server-side. Client-side verification alone is not secure.
export default {
async fetch (request : Request , env : Env ): Promise <Response > {
const formData = await request.formData ()
const token = formData.get ('cf-turnstile-response' )
const ip = request.headers .get ('CF-Connecting-IP' )
const verifyFormData = new FormData ()
verifyFormData.append ('secret' , env.TURNSTILE_SECRET_KEY )
verifyFormData.append ('response' , token)
verifyFormData.append ('remoteip' , ip)
const result = await fetch (
'https://challenges.cloudflare.com/turnstile/v0/siteverify' ,
{
method : 'POST' ,
body : verifyFormData,
}
)
const outcome = await result.json ()
if (!outcome.success ) {
return new Response ('Invalid Turnstile token' , { status : 401 })
}
return new Response ('Success!' )
}
}
The 3-Step Setup Process
Step 1: Create Widget Configuration
Log into Cloudflare Dashboard
Navigate to Turnstile section
Click "Add Site"
Configure:
Widget Mode : Managed (recommended), Non-Interactive, or Invisible
Domains : Add allowed hostnames (e.g., example.com, localhost for dev)
Name : Descriptive name (e.g., "Production Login Form")
Use separate widgets for dev/staging/production
Restrict domains to only those you control
Managed mode provides best balance of security and UX
localhost must be explicitly added for local testing
Step 2: Client-Side Integration Choose between implicit or explicit rendering:
Implicit Rendering (Recommended for static forms):
<script src ="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer > </script >
<div class ="cf-turnstile"
data-sitekey ="YOUR_SITE_KEY"
data-callback ="onSuccess"
data-error-callback ="onError" > </div >
<script >
function onSuccess (token ) {
console .log ('Turnstile success:' , token)
}
function onError (error ) {
console .error ('Turnstile error:' , error)
}
</script >
Explicit Rendering (For SPAs/dynamic UIs):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>
const widgetId = turnstile.render ('#container' , {
sitekey : 'YOUR_SITE_KEY' ,
callback : (token ) => {
console .log ('Token:' , token)
},
'error-callback' : (error ) => {
console .error ('Error:' , error)
},
theme : 'auto' ,
execution : 'render' ,
})
turnstile.reset (widgetId)
turnstile.remove (widgetId)
turnstile.execute (widgetId)
const token = turnstile.getResponse (widgetId)
React Integration (using @marsidev/react-turnstile):
import { Turnstile } from '@marsidev/react-turnstile'
export function MyForm ( ) {
const [token, setToken] = useState<string >()
return (
<form >
<Turnstile
siteKey ={TURNSTILE_SITE_KEY}
onSuccess ={setToken}
onError ={(error) => console.error(error)}
/>
<button disabled ={!token} > Submit</button >
</form >
)
}
Step 3: Server-Side Validation MANDATORY : Always call Siteverify API to validate tokens.
interface TurnstileResponse {
success : boolean
challenge_ts ?: string
hostname ?: string
error-codes ?: string []
action ?: string
cdata ?: string
}
async function validateTurnstile (
token : string ,
secretKey : string ,
options ?: {
remoteip?: string
idempotency_key?: string
expectedAction?: string
expectedHostname?: string
}
): Promise <TurnstileResponse > {
const formData = new FormData ()
formData.append ('secret' , secretKey)
formData.append ('response' , token)
if (options?.remoteip ) {
formData.append ('remoteip' , options.remoteip )
}
if (options?.idempotency_key ) {
formData.append ('idempotency_key' , options.idempotency_key )
}
const response = await fetch (
'https://challenges.cloudflare.com/turnstile/v0/siteverify' ,
{
method : 'POST' ,
body : formData,
}
)
const result = await response.json <TurnstileResponse >()
if (result.success ) {
if (options?.expectedAction && result.action !== options.expectedAction ) {
return { success : false , 'error-codes' : ['action-mismatch' ] }
}
if (options?.expectedHostname && result.hostname !== options.expectedHostname ) {
return { success : false , 'error-codes' : ['hostname-mismatch' ] }
}
}
return result
}
const result = await validateTurnstile (
token,
env.TURNSTILE_SECRET_KEY ,
{
remoteip : request.headers .get ('CF-Connecting-IP' ),
expectedHostname : 'example.com' ,
}
)
if (!result.success ) {
return new Response ('Turnstile validation failed' , { status : 401 })
}
Critical Rules
Always Do ✅ Call Siteverify API - Server-side validation is mandatory
✅ Use HTTPS - Never validate over HTTP
✅ Protect secret keys - Never expose in frontend code
✅ Handle token expiration - Tokens expire after 5 minutes
✅ Implement error callbacks - Handle failures gracefully
✅ Use dummy keys for testing - Test sitekey: 1x00000000000000000000AA
✅ Set reasonable timeouts - Don't wait indefinitely for validation
✅ Validate action/hostname - Check additional fields when specified
✅ Rotate keys periodically - Use dashboard or API to rotate secrets
✅ Monitor analytics - Track solve rates and failures
Never Do ❌ Skip server validation - Client-side only = security vulnerability
❌ Proxy api.js script - Must load from Cloudflare CDN
❌ Reuse tokens - Each token is single-use only
❌ Use GET requests - Siteverify only accepts POST
❌ Expose secret key - Keep secrets in backend environment only
❌ Trust client-side validation - Tokens can be forged
❌ Cache api.js - Future updates will break your integration
❌ Use production keys in tests - Use dummy keys instead
❌ Ignore error callbacks - Always handle failures
Known Issues Prevention This skill prevents 12 documented issues:
Issue #1: Missing Server-Side Validation Error : Zero token validation in Turnstile Analytics dashboard
Source : https://developers.cloudflare.com/turnstile/get-started/
Why It Happens : Developers only implement client-side widget, skip Siteverify call
Prevention : All templates include mandatory server-side validation with Siteverify API
Issue #2: Token Expiration (5 Minutes)
Issue #3: Secret Key Exposed in Frontend
Issue #4: GET Request to Siteverify
Issue #5: Content Security Policy Blocking
Issue #6: Widget Crash (Error 300030)
Issue #7: Configuration Error (Error 600010)
Issue #8: Safari 18 / macOS 15 "Hide IP" Issue
Issue #9: Brave Browser Confetti Animation Failure
Issue #10: Next.js + Jest Incompatibility
Issue #11: localhost Not in Allowlist
Issue #12: Token Reuse Attempt
Configuration Files Reference
wrangler.jsonc (Cloudflare Workers) {
"name" : "my-app" ,
"main" : "src/index.ts" ,
"compatibility_date" : "2025-10-22" ,
"vars" : {
"TURNSTILE_SITE_KEY" : "1x00000000000000000000AA"
} ,
"secrets" : [ "TURNSTILE_SECRET_KEY" ]
}
vars for public sitekey (visible in client code)
secrets for private secret key (encrypted, backend-only)
Use dummy keys for development (see testing-guide.md)
Rotate production secret keys quarterly
Required CSP Directives <meta http-equiv ="Content-Security-Policy" content ="
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
connect-src 'self' https://challenges.cloudflare.com;
" >
Common Patterns
Pattern 1: Hono + Cloudflare Workers import { Hono } from 'hono'
type Bindings = {
TURNSTILE_SECRET_KEY : string
TURNSTILE_SITE_KEY : string
}
const app = new Hono <{ Bindings : Bindings }>()
app.post ('/api/login' , async (c) => {
const body = await c.req .formData ()
const token = body.get ('cf-turnstile-response' )
if (!token) {
return c.text ('Missing Turnstile token' , 400 )
}
const verifyFormData = new FormData ()
verifyFormData.append ('secret' , c.env .TURNSTILE_SECRET_KEY )
verifyFormData.append ('response' , token.toString ())
verifyFormData.append ('remoteip' , c.req .header ('CF-Connecting-IP' ) || '' )
const verifyResult = await fetch (
'https://challenges.cloudflare.com/turnstile/v0/siteverify' ,
{
method : 'POST' ,
body : verifyFormData,
}
)
const outcome = await verifyResult.json <{ success : boolean }>()
if (!outcome.success ) {
return c.text ('Invalid Turnstile token' , 401 )
}
return c.json ({ message : 'Login successful' })
})
export default app
When to use : API routes in Cloudflare Workers with Hono framework
Pattern 2: React + Next.js App Router 'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export function ContactForm ( ) {
const [token, setToken] = useState<string >()
const [error, setError] = useState<string >()
async function handleSubmit (e : React .FormEvent <HTMLFormElement > ) {
e.preventDefault ()
if (!token) {
setError ('Please complete the challenge' )
return
}
const formData = new FormData (e.currentTarget )
formData.append ('cf-turnstile-response' , token)
const response = await fetch ('/api/contact' , {
method : 'POST' ,
body : formData,
})
if (!response.ok ) {
setError ('Submission failed' )
return
}
}
return (
<form onSubmit ={handleSubmit} >
<input name ="email" type ="email" required />
<textarea name ="message" required />
<Turnstile
siteKey ={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onSuccess ={setToken}
onError ={() => setError('Challenge failed')}
onExpire={() => setToken(undefined)}
/>
{error && <div className ="error" > {error}</div > }
<button type ="submit" disabled ={!token} >
Submit
</button >
</form >
)
}
When to use : Client-side forms in Next.js with React hooks
Pattern 3: E2E Testing with Dummy Keys
export const TEST_TURNSTILE = {
sitekey : {
alwaysPass : '1x00000000000000000000AA' ,
alwaysBlock : '2x00000000000000000000AB' ,
invisible : '1x00000000000000000000BB' ,
interactive : '3x00000000000000000000FF' ,
},
secretKey : {
alwaysPass : '1x0000000000000000000000000000000AA' ,
alwaysFail : '2x0000000000000000000000000000000AA' ,
tokenSpent : '3x0000000000000000000000000000000AA' ,
},
dummyToken : 'XXXX.DUMMY.TOKEN.XXXX' ,
}
test ('form submission with Turnstile' , async ({ page }) => {
await page.goto ('/contact?test=true' )
await page.fill ('input[name="email"]' , 'test@example.com' )
await page.click ('button[type="submit"]' )
await expect (page.locator ('.success' )).toBeVisible ()
})
When to use : Automated testing (Playwright, Cypress, Jest)
Pattern 4: Widget Lifecycle Management class TurnstileManager {
private widgetId : string | null = null
private sitekey : string
constructor (sitekey : string ) {
this .sitekey = sitekey
}
render (containerId : string , callbacks : {
onSuccess: (token: string ) => void
onError: (error: string ) => void
} ) {
if (this .widgetId !== null ) {
this .reset ()
}
this .widgetId = turnstile.render (containerId, {
sitekey : this .sitekey ,
callback : callbacks.onSuccess ,
'error-callback' : callbacks.onError ,
'expired-callback' : () => this .reset (),
})
return this .widgetId
}
reset ( ) {
if (this .widgetId !== null ) {
turnstile.reset (this .widgetId )
}
}
remove ( ) {
if (this .widgetId !== null ) {
turnstile.remove (this .widgetId )
this .widgetId = null
}
}
getToken (): string | undefined {
if (this .widgetId === null ) return undefined
return turnstile.getResponse (this .widgetId )
}
}
const manager = new TurnstileManager (SITE_KEY )
manager.render ('#container' , {
onSuccess : (token ) => console .log ('Token:' , token),
onError : (error ) => console .error ('Error:' , error),
})
When to use : SPAs requiring programmatic widget control
Using Bundled Resources
Scripts (scripts/)
check-csp.sh - Verifies Content Security Policy allows Turnstile scripts and iframes
./scripts/check-csp.sh https://example.com
References (references/)
references/widget-configs.md - Complete reference of all widget configuration options
references/error-codes.md - Comprehensive error code reference with troubleshooting
references/testing-guide.md - Testing strategies, dummy keys, E2E patterns
references/react-integration.md - React-specific patterns and @marsidev/react-turnstile usage
When Claude should load these :
widget-configs.md: When configuring widget appearance, themes, or execution modes
error-codes.md: When debugging error codes 100*, 200*, 300*, 400*, 600*
testing-guide.md: When setting up E2E tests or local development
react-integration.md: When integrating with React, Next.js, or encountering React-specific issues
Templates (templates/)
wrangler-turnstile-config.jsonc - Cloudflare Workers environment configuration
turnstile-widget-implicit.html - Implicit rendering HTML example
turnstile-widget-explicit.ts - Explicit rendering JavaScript API
turnstile-server-validation.ts - Siteverify API validation function
turnstile-react-component.tsx - React component using @marsidev/react-turnstile
turnstile-hono-route.ts - Hono route handler with validation
turnstile-test-config.ts - Testing configuration with dummy keys
Advanced Topics
Pre-Clearance for SPAs Turnstile can issue a pre-clearance cookie that persists across page navigations in single-page applications.
turnstile.render ('#container' , {
sitekey : SITE_KEY ,
callback : async (token) => {
await fetch ('/api/pre-clearance' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({ token }),
})
},
})
Custom Actions and cData Track different challenge types or pass custom data:
turnstile.render ('#container' , {
sitekey : SITE_KEY ,
action : 'login' ,
cdata : JSON .stringify ({ userId : '123' }),
callback : (token ) => {
},
})
Server-side verification:
const result = await validateTurnstile (token, secretKey)
if (result.action !== 'login' ) {
return new Response ('Invalid action' , { status : 400 })
}
const customData = JSON .parse (result.cdata || '{}' )
Retry and Error Handling Strategies class TurnstileWithRetry {
private retryCount = 0
private maxRetries = 3
render (containerId : string ) {
turnstile.render (containerId, {
sitekey : SITE_KEY ,
retry : 'auto' ,
'retry-interval' : 8000 ,
'error-callback' : (error ) => {
this .handleError (error)
},
})
}
private handleError (error : string ) {
const noRetry = ['110100' , '110200' , '110500' ]
if (noRetry.some (code => error.includes (code))) {
this .showFallback ()
return
}
if (this .retryCount < this .maxRetries ) {
this .retryCount ++
setTimeout (() => {
turnstile.reset (this .widgetId )
}, 2000 * this .retryCount )
} else {
this .showFallback ()
}
}
private showFallback ( ) {
console .error ('Turnstile failed - showing fallback' )
}
}
Multi-Widget Pages const widgets = {
login : null as string | null ,
signup : null as string | null ,
}
widgets.login = turnstile.render ('#login-widget' , {
sitekey : SITE_KEY ,
action : 'login' ,
})
widgets.signup = turnstile.render ('#signup-widget' , {
sitekey : SITE_KEY ,
action : 'signup' ,
})
turnstile.reset (widgets.login )
const loginToken = turnstile.getResponse (widgets.login )
Dependencies
None (Turnstile loads from CDN)
@marsidev/react-turnstile@1.3.1 - Official Cloudflare-recommended React integration
turnstile-types@1.2.3 - TypeScript type definitions
Optional (Other Frameworks):
vue-turnstile - Vue 3 integration
cfturnstile-vue3 - Alternative Vue 3 wrapper
ngx-turnstile - Angular integration
svelte-turnstile - Svelte integration
@nuxtjs/turnstile - Nuxt full-stack integration
Official Documentation
Package Versions (Verified 2025-10-22) {
"devDependencies" : {
"@marsidev/react-turnstile" : "^1.3.1" ,
"turnstile-types" : "^1.2.3"
}
}
@marsidev/react-turnstile is Cloudflare's recommended React package
Last updated September 2025 (actively maintained)
Compatible with React 18+, Next.js 13+, Next.js 14+, Next.js 15+
Production Example This skill is based on production implementations:
Cloudflare Workers : Official HTMLRewriter example
React Apps : @marsidev/react-turnstile (Cloudflare-verified)
Community : WordPress, Craft CMS, SilverStripe, Statamic integrations
Validation : ✅ All 12 known issues documented and prevented
Troubleshooting
Problem: Error 110200 - "Unknown domain" Solution : Add your domain (including localhost for dev) to widget's allowed domains in Cloudflare Dashboard. For local dev, use dummy test sitekey 1x00000000000000000000AA instead.
Problem: Error 300030 - Widget crashes for legitimate users Solution : Implement error callback with retry logic. This is a known Cloudflare-side issue (2025). Fallback to alternative verification if retries fail.
Problem: Tokens always return success: false
Check token hasn't expired (5 min TTL)
Verify secret key is correct
Ensure token hasn't been validated before (single-use)
Check hostname matches widget configuration
Problem: CSP blocking iframe (Error 200500) Solution : Add CSP directives:
<meta http-equiv ="Content-Security-Policy" content ="
frame-src https://challenges.cloudflare.com;
script-src https://challenges.cloudflare.com;
" >
Problem: Safari 18 "Hide IP" causing Error 300010 Solution : Document in error message that users should disable Safari's "Hide IP address" setting (Safari → Settings → Privacy → Hide IP address → Off)
Problem: Next.js + Jest tests failing with @marsidev/react-turnstile Solution : Mock the Turnstile component in Jest setup:
jest.mock ('@marsidev/react-turnstile' , () => ({
Turnstile : () => <div data-testid ="turnstile-mock" /> ,
}))
Complete Setup Checklist Use this checklist to verify your setup:
Check references/error-codes.md for specific error troubleshooting
Verify all steps in the 3-Step Setup Process
Check official docs: https://developers.cloudflare.com/turnstile/
Ensure server-side validation is implemented (most common issue)
Use Cloudflare Docs MCP tool: mcp__cloudflare-docs__search_cloudflare_documentation
Token Efficiency : ~65-70% savings (10-12k tokens → 3-4k tokens)
Errors Prevented : 12 documented issues with complete solutions