commit 0aa89c365d2bfb8be38415415ac7cc4562c29358 Author: Zhongwei Li Date: Sun Nov 30 08:24:31 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..9a1582f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "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*.", + "version": "1.0.0", + "author": { + "name": "Jeremy Dawes", + "email": "jeremy@jezweb.net" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..120cea2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 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*. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..b4d38ce --- /dev/null +++ b/SKILL.md @@ -0,0 +1,432 @@ +--- +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-11-24 +**Dependencies**: None (optional: @marsidev/react-turnstile for React) +**Latest Versions**: @marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3 + +**Recent Updates (2025)**: +- **March 2025**: Upgraded Turnstile Analytics with TopN statistics (7 dimensions: hostnames, browsers, countries, user agents, ASNs, OS, source IPs), anomaly detection, enhanced bot behavior monitoring +- **2025**: WCAG 2.1 AA compliance, Free plan (20 widgets, 7-day analytics), Enterprise features (unlimited widgets, ephemeral IDs, any hostname support, 30-day analytics, offlabel branding) + +--- + +## Quick Start (5 Minutes) + +```bash +# 1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile +# Copy sitekey (public) and secret key (private) + +# 2. Add widget to frontend + +
+
+ +
+ +# 3. Validate token server-side (Cloudflare Workers) +const formData = await request.formData() +const token = formData.get('cf-turnstile-response') + +const verifyFormData = new FormData() +verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY) +verifyFormData.append('response', token) +verifyFormData.append('remoteip', request.headers.get('CF-Connecting-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', { status: 401 }) +``` + +**CRITICAL:** +- Token expires in 5 minutes, single-use only +- ALWAYS validate server-side (Siteverify API required) +- Never proxy/cache api.js (must load from Cloudflare CDN) +- Use different widgets for dev/staging/production + +## Rendering Modes + +**Implicit** (auto-render on page load): +```html + +
+``` + +**Explicit** (programmatic control for SPAs): +```typescript + +const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' }) +turnstile.reset(widgetId) // Reset widget +turnstile.getResponse(widgetId) // Get token +``` + +**React** (using @marsidev/react-turnstile): +```tsx +import { Turnstile } from '@marsidev/react-turnstile' + +``` + +--- + +## 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) +**Error**: `success: false` for valid tokens submitted after delay +**Source**: https://developers.cloudflare.com/turnstile/get-started/server-side-validation +**Why It Happens**: Tokens expire 300 seconds after generation +**Prevention**: Templates document TTL and implement token refresh on expiration + +### Issue #3: Secret Key Exposed in Frontend +**Error**: Security bypass - attackers can validate their own tokens +**Source**: https://developers.cloudflare.com/turnstile/get-started/server-side-validation +**Why It Happens**: Secret key hardcoded in JavaScript or visible in source +**Prevention**: All templates show backend-only validation with environment variables + +### Issue #4: GET Request to Siteverify +**Error**: API returns 405 Method Not Allowed +**Source**: https://developers.cloudflare.com/turnstile/migration/recaptcha +**Why It Happens**: reCAPTCHA supports GET, Turnstile requires POST +**Prevention**: Templates use POST with FormData or JSON body + +### Issue #5: Content Security Policy Blocking +**Error**: Error 200500 - "Loading error: The iframe could not be loaded" +**Source**: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes +**Why It Happens**: CSP blocks challenges.cloudflare.com iframe +**Prevention**: Skill includes CSP configuration reference and check-csp.sh script + +### Issue #6: Widget Crash (Error 300030) +**Error**: Generic client execution error for legitimate users +**Source**: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 +**Why It Happens**: Unknown - appears to be Cloudflare-side issue (2025) +**Prevention**: Templates implement error callbacks, retry logic, and fallback handling + +### Issue #7: Configuration Error (Error 600010) +**Error**: Widget fails with "configuration error" +**Source**: https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 +**Why It Happens**: Missing or deleted hostname in widget configuration +**Prevention**: Templates document hostname allowlist requirement and verification steps + +### Issue #8: Safari 18 / macOS 15 "Hide IP" Issue +**Error**: Error 300010 when Safari's "Hide IP address" is enabled +**Source**: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 +**Why It Happens**: Privacy settings interfere with challenge signals +**Prevention**: Error handling reference documents Safari workaround (disable Hide IP) + +### Issue #9: Brave Browser Confetti Animation Failure +**Error**: Verification fails during success animation +**Source**: https://github.com/brave/brave-browser/issues/45608 (April 2025) +**Why It Happens**: Brave shields block animation scripts +**Prevention**: Templates handle success before animation completes + +### Issue #10: Next.js + Jest Incompatibility +**Error**: @marsidev/react-turnstile breaks Jest tests +**Source**: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025) +**Why It Happens**: Module resolution issues with Jest +**Prevention**: Testing guide includes Jest mocking patterns and dummy sitekey usage + +### Issue #11: localhost Not in Allowlist +**Error**: Error 110200 - "Unknown domain: Domain not allowed" +**Source**: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes +**Why It Happens**: Production widget used in development without localhost in allowlist +**Prevention**: Templates use dummy test keys for dev, document localhost allowlist requirement + +### Issue #12: Token Reuse Attempt +**Error**: `success: false` with "token already spent" error +**Source**: https://developers.cloudflare.com/turnstile/troubleshooting/testing +**Why It Happens**: Each token can only be validated once +**Prevention**: Templates document single-use constraint and token refresh patterns + +## Configuration + +**wrangler.jsonc:** +```jsonc +{ + "vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" }, + "secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY +} +``` + +**Required CSP:** +```html + +``` + +--- + +## Common Patterns + +### Pattern 1: Hono + Cloudflare Workers + +```typescript +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) + } + + // Validate token + 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) + } + + // Process login + 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 + +```tsx +'use client' + +import { Turnstile } from '@marsidev/react-turnstile' +import { useState } from 'react' + +export function ContactForm() { + const [token, setToken] = useState() + const [error, setError] = useState() + + async function handleSubmit(e: React.FormEvent) { + 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 + } + + // Success + } + + return ( +
+ + + + + +
+ + + +
+
+ + + + diff --git a/templates/wrangler-turnstile-config.jsonc b/templates/wrangler-turnstile-config.jsonc new file mode 100644 index 0000000..0afa58b --- /dev/null +++ b/templates/wrangler-turnstile-config.jsonc @@ -0,0 +1,36 @@ +{ + "name": "my-turnstile-app", + "main": "src/index.ts", + "compatibility_date": "2025-10-22", + + // Public sitekey - safe to commit to version control + // Use dummy keys for development, real keys for production + "vars": { + "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" // Test key - always passes + // Production: Replace with your real sitekey from https://dash.cloudflare.com/?to=/:account/turnstile + }, + + // Secret key - NEVER commit to version control + // Set using: wrangler secret put TURNSTILE_SECRET_KEY + "secrets": ["TURNSTILE_SECRET_KEY"], + + // Optional: Environment-specific configuration + "env": { + "production": { + "vars": { + "TURNSTILE_SITE_KEY": "" + } + }, + "staging": { + "vars": { + "TURNSTILE_SITE_KEY": "" + } + }, + "development": { + "vars": { + // Use test sitekey for development (always passes) + "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" + } + } + } +}