update skills

This commit is contained in:
2026-03-17 16:53:22 -07:00
parent 0b0783ef8e
commit f9a530667e
389 changed files with 54512 additions and 1 deletions

View File

@@ -0,0 +1,99 @@
# Cloudflare Turnstile Implementation Skill Reference
Expert guidance for implementing Cloudflare Turnstile - a smart CAPTCHA alternative that protects websites from bots without showing traditional CAPTCHA puzzles.
## Overview
Turnstile is a user-friendly CAPTCHA alternative that runs challenges in the background without user interaction. It validates visitors automatically using signals like browser behavior, device fingerprinting, and machine learning.
## Widget Types
| Type | Interaction | Use Case |
|------|-------------|----------|
| **Managed** (default) | Shows checkbox when needed | Forms, logins - balance UX and security |
| **Non-Interactive** | Invisible, runs automatically | Frictionless UX, low-risk actions |
| **Invisible** | Hidden, triggered programmatically | Pre-clearance, API calls, headless |
## Quick Start
### Implicit Rendering (HTML-based)
```html
<!-- 1. Add script -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- 2. Add widget to form -->
<form action="/submit" method="POST">
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
```
### Explicit Rendering (JavaScript-based)
```html
<div id="turnstile-container"></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<script>
window.turnstile.render('#turnstile-container', {
sitekey: 'YOUR_SITE_KEY',
callback: (token) => console.log('Token:', token)
});
</script>
```
### Server Validation (Required)
```javascript
// Cloudflare Workers
export default {
async fetch(request) {
const formData = await request.formData();
const token = formData.get('cf-turnstile-response');
const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: env.TURNSTILE_SECRET,
response: token,
remoteip: request.headers.get('CF-Connecting-IP')
})
});
const validation = await result.json();
if (!validation.success) {
return new Response('Invalid CAPTCHA', { status: 400 });
}
// Process form...
}
}
```
## Testing Keys
**Critical for development/testing:**
| Type | Key | Behavior |
|------|-----|----------|
| **Site Key (Always Passes)** | `1x00000000000000000000AA` | Widget succeeds, token validates |
| **Site Key (Always Blocks)** | `2x00000000000000000000AB` | Widget fails visibly |
| **Site Key (Force Challenge)** | `3x00000000000000000000FF` | Always shows interactive challenge |
| **Secret Key (Testing)** | `1x0000000000000000000000000000000AA` | Validates test tokens |
**Note:** Test keys work on `localhost` and any domain. Do NOT use in production.
## Key Constraints
- **Token expiry:** 5 minutes after generation
- **Single-use:** Each token can only be validated once
- **Server validation required:** Client-side checks are insufficient
## Reading Order
1. **[configuration.md](configuration.md)** - Setup, widget options, script loading
2. **[api.md](api.md)** - JavaScript API, siteverify endpoints, TypeScript types
3. **[patterns.md](patterns.md)** - Form integration, framework examples, validation patterns
4. **[gotchas.md](gotchas.md)** - Common errors, debugging, limitations
## See Also
- [Cloudflare Turnstile Docs](https://developers.cloudflare.com/turnstile/)
- [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)

View File

@@ -0,0 +1,240 @@
# API Reference
## Client-Side JavaScript API
The Turnstile JavaScript API is available at `window.turnstile` after loading the script.
### `turnstile.render(container, options)`
Renders a Turnstile widget into a container element.
**Parameters:**
- `container` (string | HTMLElement): CSS selector or DOM element
- `options` (TurnstileOptions): Configuration object (see [configuration.md](configuration.md))
**Returns:** `string` - Widget ID for use with other API methods
**Example:**
```javascript
const widgetId = window.turnstile.render('#my-container', {
sitekey: 'YOUR_SITE_KEY',
callback: (token) => console.log('Success:', token),
'error-callback': (code) => console.error('Error:', code)
});
```
### `turnstile.reset(widgetId)`
Resets a widget (clears token, resets challenge state). Useful when form validation fails.
**Parameters:**
- `widgetId` (string): Widget ID from `render()`, or container element
**Returns:** `void`
**Example:**
```javascript
// Reset on form error
if (!validateForm()) {
window.turnstile.reset(widgetId);
}
```
### `turnstile.remove(widgetId)`
Removes a widget from the DOM completely.
**Parameters:**
- `widgetId` (string): Widget ID from `render()`
**Returns:** `void`
**Example:**
```javascript
// Cleanup on navigation
window.turnstile.remove(widgetId);
```
### `turnstile.getResponse(widgetId)`
Gets the current token from a widget (if challenge completed).
**Parameters:**
- `widgetId` (string): Widget ID from `render()`, or container element
**Returns:** `string | undefined` - Token string, or undefined if not ready
**Example:**
```javascript
const token = window.turnstile.getResponse(widgetId);
if (token) {
submitForm(token);
}
```
### `turnstile.isExpired(widgetId)`
Checks if a widget's token has expired (>5 minutes old).
**Parameters:**
- `widgetId` (string): Widget ID from `render()`
**Returns:** `boolean` - True if expired
**Example:**
```javascript
if (window.turnstile.isExpired(widgetId)) {
window.turnstile.reset(widgetId);
}
```
## Callback Signatures
```typescript
type TurnstileCallback = (token: string) => void;
type ErrorCallback = (errorCode: string) => void;
type TimeoutCallback = () => void;
type ExpiredCallback = () => void;
type BeforeInteractiveCallback = () => void;
type AfterInteractiveCallback = () => void;
type UnsupportedCallback = () => void;
```
## Siteverify API (Server-Side)
**Endpoint:** `https://challenges.cloudflare.com/turnstile/v0/siteverify`
### Request
**Method:** POST
**Content-Type:** `application/json` or `application/x-www-form-urlencoded`
```typescript
interface SiteverifyRequest {
secret: string; // Your secret key (never expose client-side)
response: string; // Token from cf-turnstile-response
remoteip?: string; // User's IP (optional but recommended)
idempotency_key?: string; // Unique key for idempotent validation
}
```
**Example:**
```javascript
// Cloudflare Workers
const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: env.TURNSTILE_SECRET,
response: token,
remoteip: request.headers.get('CF-Connecting-IP')
})
});
const data = await result.json();
```
### Response
```typescript
interface SiteverifyResponse {
success: boolean; // Validation result
challenge_ts?: string; // ISO timestamp of challenge
hostname?: string; // Hostname where widget was solved
'error-codes'?: string[]; // Error codes if success=false
action?: string; // Action name from widget config
cdata?: string; // Custom data from widget config
}
```
**Example Success:**
```json
{
"success": true,
"challenge_ts": "2024-01-15T10:30:00Z",
"hostname": "example.com",
"action": "login",
"cdata": "user123"
}
```
**Example Failure:**
```json
{
"success": false,
"error-codes": ["timeout-or-duplicate"]
}
```
## Error Codes
| Code | Cause | Solution |
|------|-------|----------|
| `missing-input-secret` | Secret key not provided | Include `secret` in request |
| `invalid-input-secret` | Secret key is wrong | Check secret key in dashboard |
| `missing-input-response` | Token not provided | Include `response` token |
| `invalid-input-response` | Token is invalid/malformed | Verify token from widget |
| `timeout-or-duplicate` | Token expired (>5min) or reused | Generate new token, validate once |
| `internal-error` | Cloudflare server error | Retry with exponential backoff |
| `bad-request` | Malformed request | Check JSON/form encoding |
## TypeScript Types
```typescript
interface TurnstileOptions {
sitekey: string;
action?: string;
cData?: string;
callback?: (token: string) => void;
'error-callback'?: (errorCode: string) => void;
'expired-callback'?: () => void;
'timeout-callback'?: () => void;
'before-interactive-callback'?: () => void;
'after-interactive-callback'?: () => void;
'unsupported-callback'?: () => void;
theme?: 'light' | 'dark' | 'auto';
size?: 'normal' | 'compact' | 'flexible';
tabindex?: number;
'response-field'?: boolean;
'response-field-name'?: string;
retry?: 'auto' | 'never';
'retry-interval'?: number;
language?: string;
execution?: 'render' | 'execute';
appearance?: 'always' | 'execute' | 'interaction-only';
'refresh-expired'?: 'auto' | 'manual' | 'never';
}
interface Turnstile {
render(container: string | HTMLElement, options: TurnstileOptions): string;
reset(widgetId: string): void;
remove(widgetId: string): void;
getResponse(widgetId: string): string | undefined;
isExpired(widgetId: string): boolean;
execute(container?: string | HTMLElement, options?: TurnstileOptions): void;
}
declare global {
interface Window {
turnstile: Turnstile;
onloadTurnstileCallback?: () => void;
}
}
```
## Script Loading
```html
<!-- Standard -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- Explicit render mode -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<!-- With load callback -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback"></script>
<script>
window.onloadTurnstileCallback = () => {
window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' });
};
</script>
```

View File

@@ -0,0 +1,222 @@
# Configuration
## Script Loading
### Basic (Implicit Rendering)
```html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
```
Automatically renders widgets with `class="cf-turnstile"` on page load.
### Explicit Rendering
```html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
```
Manual control over when/where widgets render via `window.turnstile.render()`.
### With Load Callback
```html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=myCallback"></script>
<script>
function myCallback() {
// API ready
window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' });
}
</script>
```
### Compatibility Mode
```html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?compat=recaptcha"></script>
```
Provides `grecaptcha` API for Google reCAPTCHA drop-in replacement.
## Widget Configuration
### Complete Options Object
```javascript
{
// Required
sitekey: 'YOUR_SITE_KEY', // Widget sitekey from dashboard
// Callbacks
callback: (token) => {}, // Success - token ready
'error-callback': (code) => {}, // Error occurred
'expired-callback': () => {}, // Token expired (>5min)
'timeout-callback': () => {}, // Challenge timeout
'before-interactive-callback': () => {}, // Before showing checkbox
'after-interactive-callback': () => {}, // After user interacts
'unsupported-callback': () => {}, // Browser doesn't support Turnstile
// Appearance
theme: 'auto', // 'light' | 'dark' | 'auto'
size: 'normal', // 'normal' | 'compact' | 'flexible'
tabindex: 0, // Tab order (accessibility)
language: 'auto', // ISO 639-1 code or 'auto'
// Behavior
execution: 'render', // 'render' (auto) | 'execute' (manual)
appearance: 'always', // 'always' | 'execute' | 'interaction-only'
retry: 'auto', // 'auto' | 'never'
'retry-interval': 8000, // Retry interval (ms), default 8000
'refresh-expired': 'auto', // 'auto' | 'manual' | 'never'
// Form Integration
'response-field': true, // Add hidden input (default: true)
'response-field-name': 'cf-turnstile-response', // Hidden input name
// Analytics & Data
action: 'login', // Action name (for analytics)
cData: 'user-session-123', // Custom data (returned in siteverify)
}
```
### Key Options Explained
**`execution`:**
- `'render'` (default): Challenge starts immediately on render
- `'execute'`: Wait for `turnstile.execute()` call
**`appearance`:**
- `'always'` (default): Widget always visible
- `'execute'`: Hidden until `execute()` called
- `'interaction-only'`: Hidden until user interaction needed
**`refresh-expired`:**
- `'auto'` (default): Auto-refresh expired tokens
- `'manual'`: App must call `reset()` after expiry
- `'never'`: No refresh, expired-callback triggered
**`retry`:**
- `'auto'` (default): Auto-retry failed challenges
- `'never'`: Don't retry, trigger error-callback
## HTML Data Attributes
For implicit rendering, use data attributes on `<div class="cf-turnstile">`:
| JavaScript Property | HTML Data Attribute | Example |
|---------------------|---------------------|---------|
| `sitekey` | `data-sitekey` | `data-sitekey="YOUR_KEY"` |
| `action` | `data-action` | `data-action="login"` |
| `cData` | `data-cdata` | `data-cdata="session-123"` |
| `callback` | `data-callback` | `data-callback="onSuccess"` |
| `error-callback` | `data-error-callback` | `data-error-callback="onError"` |
| `expired-callback` | `data-expired-callback` | `data-expired-callback="onExpired"` |
| `timeout-callback` | `data-timeout-callback` | `data-timeout-callback="onTimeout"` |
| `theme` | `data-theme` | `data-theme="dark"` |
| `size` | `data-size` | `data-size="compact"` |
| `tabindex` | `data-tabindex` | `data-tabindex="0"` |
| `response-field` | `data-response-field` | `data-response-field="false"` |
| `response-field-name` | `data-response-field-name` | `data-response-field-name="token"` |
| `retry` | `data-retry` | `data-retry="never"` |
| `retry-interval` | `data-retry-interval` | `data-retry-interval="5000"` |
| `language` | `data-language` | `data-language="en"` |
| `execution` | `data-execution` | `data-execution="execute"` |
| `appearance` | `data-appearance` | `data-appearance="interaction-only"` |
| `refresh-expired` | `data-refresh-expired` | `data-refresh-expired="manual"` |
**Example:**
```html
<div class="cf-turnstile"
data-sitekey="YOUR_SITE_KEY"
data-theme="dark"
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError"></div>
```
## Content Security Policy
Add these directives to CSP header/meta tag:
```
script-src https://challenges.cloudflare.com;
frame-src https://challenges.cloudflare.com;
```
**Full Example:**
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://challenges.cloudflare.com;
frame-src https://challenges.cloudflare.com;">
```
## Framework-Specific Setup
### React
```bash
npm install @marsidev/react-turnstile
```
```jsx
import Turnstile from '@marsidev/react-turnstile';
<Turnstile
siteKey="YOUR_SITE_KEY"
onSuccess={(token) => console.log(token)}
/>
```
### Vue
```bash
npm install vue-turnstile
```
```vue
<template>
<VueTurnstile site-key="YOUR_SITE_KEY" @success="onSuccess" />
</template>
<script setup>
import VueTurnstile from 'vue-turnstile';
</script>
```
### Svelte
```bash
npm install svelte-turnstile
```
```svelte
<script>
import Turnstile from 'svelte-turnstile';
</script>
<Turnstile siteKey="YOUR_SITE_KEY" on:turnstile-callback={handleToken} />
```
### Next.js (App Router)
```tsx
// app/components/TurnstileWidget.tsx
'use client';
import { useEffect, useRef } from 'react';
export default function TurnstileWidget({ sitekey, onSuccess }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current && window.turnstile) {
const widgetId = window.turnstile.render(ref.current, {
sitekey,
callback: onSuccess
});
return () => window.turnstile.remove(widgetId);
}
}, [sitekey, onSuccess]);
return <div ref={ref} />;
}
```
## Cloudflare Pages Plugin
```bash
npm install @cloudflare/pages-plugin-turnstile
```
```typescript
// functions/_middleware.ts
import turnstilePlugin from '@cloudflare/pages-plugin-turnstile';
export const onRequest = turnstilePlugin({
secret: 'YOUR_SECRET_KEY',
onError: () => new Response('CAPTCHA failed', { status: 403 })
});
```

View File

@@ -0,0 +1,218 @@
# Troubleshooting & Gotchas
## Critical Rules
### ❌ Skipping Server-Side Validation
**Problem:** Client-only validation is easily bypassed.
**Solution:** Always validate on server.
```javascript
// CORRECT - Server validates token
app.post('/submit', async (req, res) => {
const token = req.body['cf-turnstile-response'];
const validation = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: JSON.stringify({ secret: SECRET, response: token })
}).then(r => r.json());
if (!validation.success) return res.status(403).json({ error: 'CAPTCHA failed' });
});
```
### ❌ Exposing Secret Key
**Problem:** Secret key leaked in client-side code.
**Solution:** Server-side validation only. Never send secret to client.
### ❌ Reusing Tokens (Single-Use Rule)
**Problem:** Tokens are single-use. Revalidation fails with `timeout-or-duplicate`.
**Solution:** Generate new token for each submission. Reset widget on error.
```javascript
if (!response.ok) window.turnstile.reset(widgetId);
```
### ❌ Not Handling Token Expiry
**Problem:** Tokens expire after 5 minutes.
**Solution:** Handle expiry callback or use auto-refresh.
```javascript
window.turnstile.render('#container', {
sitekey: 'YOUR_SITE_KEY',
'refresh-expired': 'auto', // or 'manual' with expired-callback
'expired-callback': () => window.turnstile.reset(widgetId)
});
```
## Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| **Widget not rendering** | Incorrect sitekey, CSP blocking, file:// protocol | Check sitekey, add CSP for challenges.cloudflare.com, use http:// |
| **timeout-or-duplicate** | Token expired (>5min) or reused | Generate fresh token, don't cache >5min |
| **invalid-input-secret** | Wrong secret key | Verify secret from dashboard, check env vars |
| **missing-input-response** | Token not sent | Check form field name is 'cf-turnstile-response' |
## Framework Gotchas
### React: Widget Re-mounting
**Problem:** Widget re-renders on state change, losing token.
**Solution:** Control lifecycle with useRef.
```tsx
function TurnstileWidget({ onToken }) {
const containerRef = useRef(null);
const widgetIdRef = useRef(null);
useEffect(() => {
if (containerRef.current && !widgetIdRef.current) {
widgetIdRef.current = window.turnstile.render(containerRef.current, {
sitekey: 'YOUR_SITE_KEY',
callback: onToken
});
}
return () => {
if (widgetIdRef.current) {
window.turnstile.remove(widgetIdRef.current);
widgetIdRef.current = null;
}
};
}, []);
return <div ref={containerRef} />;
}
```
### React StrictMode: Double Render
**Problem:** Widget renders twice in dev due to StrictMode.
**Solution:** Use cleanup function.
```tsx
useEffect(() => {
const widgetId = window.turnstile.render('#container', { sitekey });
return () => window.turnstile.remove(widgetId);
}, []);
```
### Next.js: SSR Hydration
**Problem:** `window.turnstile` undefined during SSR.
**Solution:** Use `'use client'` or dynamic import with `ssr: false`.
```tsx
'use client';
export default function Turnstile() { /* component */ }
```
### SPA: Navigation Without Cleanup
**Problem:** Navigating leaves orphaned widgets.
**Solution:** Remove widget in cleanup.
```javascript
// Vue
onBeforeUnmount(() => window.turnstile.remove(widgetId));
// React
useEffect(() => () => window.turnstile.remove(widgetId), []);
```
## Network & Security
### CSP Blocking
**Problem:** Content Security Policy blocks script/iframe.
**Solution:** Add CSP directives.
```html
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://challenges.cloudflare.com;
frame-src https://challenges.cloudflare.com;">
```
### IP Address Forwarding
**Problem:** Server receives proxy IP instead of client IP.
**Solution:** Use correct header.
```javascript
// Cloudflare Workers
const ip = request.headers.get('CF-Connecting-IP');
// Behind proxy
const ip = request.headers.get('X-Forwarded-For')?.split(',')[0];
```
### CORS (Siteverify)
**Problem:** CORS error calling siteverify from browser.
**Solution:** Never call siteverify client-side. Call your backend, backend calls siteverify.
## Limits & Constraints
| Limit | Value | Impact |
|-------|-------|--------|
| Token validity | 5 minutes | Must regenerate after expiry |
| Token use | Single-use | Cannot revalidate same token |
| Widget size | 300x65px (normal), 130x120px (compact) | Plan layout |
## Debugging
### Console Logging
```javascript
window.turnstile.render('#container', {
sitekey: 'YOUR_SITE_KEY',
callback: (token) => console.log('✓ Token:', token),
'error-callback': (code) => console.error('✗ Error:', code),
'expired-callback': () => console.warn('⏱ Expired'),
'timeout-callback': () => console.warn('⏱ Timeout')
});
```
### Check Token State
```javascript
const token = window.turnstile.getResponse(widgetId);
console.log('Token:', token || 'NOT READY');
console.log('Expired:', window.turnstile.isExpired(widgetId));
```
### Test Keys (Use First)
Always develop with test keys before production:
- Site: `1x00000000000000000000AA`
- Secret: `1x0000000000000000000000000000000AA`
### Network Tab
- Verify `api.js` loads (200 OK)
- Check siteverify request/response
- Look for 4xx/5xx errors
## Misconfigurations
### Wrong Key Pairing
**Problem:** Site key from one widget, secret from another.
**Solution:** Verify site key and secret are from same widget in dashboard.
### Test Keys in Production
**Problem:** Using test keys in production.
**Solution:** Environment-based keys.
```javascript
const SITE_KEY = process.env.NODE_ENV === 'production'
? process.env.TURNSTILE_SITE_KEY
: '1x00000000000000000000AA';
```
### Missing Environment Variables
**Problem:** Secret undefined on server.
**Solution:** Check .env and verify loading.
```bash
# .env
TURNSTILE_SECRET=your_secret_here
# Verify
console.log('Secret loaded:', !!process.env.TURNSTILE_SECRET);
```
## Reference
- [Turnstile Docs](https://developers.cloudflare.com/turnstile/)
- [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)
- [Error Codes](https://developers.cloudflare.com/turnstile/troubleshooting/)

View File

@@ -0,0 +1,193 @@
# Common Patterns
## Form Integration
### Basic Form (Implicit Rendering)
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
<form action="/submit" method="POST">
<input type="email" name="email" required>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
</body>
</html>
```
### Controlled Form (Explicit Rendering)
```javascript
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<script>
let widgetId = window.turnstile.render('#container', {
sitekey: 'YOUR_SITE_KEY',
callback: (token) => console.log('Token:', token)
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const token = window.turnstile.getResponse(widgetId);
if (!token) return;
const response = await fetch('/submit', {
method: 'POST',
body: JSON.stringify({ 'cf-turnstile-response': token })
});
if (!response.ok) window.turnstile.reset(widgetId);
});
</script>
```
## Framework Patterns
### React
```tsx
import { useState } from 'react';
import Turnstile from '@marsidev/react-turnstile';
export default function Form() {
const [token, setToken] = useState<string | null>(null);
return (
<form onSubmit={async (e) => {
e.preventDefault();
if (!token) return;
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ 'cf-turnstile-response': token })
});
}}>
<Turnstile siteKey="YOUR_SITE_KEY" onSuccess={setToken} />
<button disabled={!token}>Submit</button>
</form>
);
}
```
### Vue / Svelte
```vue
<!-- Vue: npm install vue-turnstile -->
<VueTurnstile :site-key="SITE_KEY" @success="token = $event" />
<!-- Svelte: npm install svelte-turnstile -->
<Turnstile siteKey={SITE_KEY} on:turnstile-callback={(e) => token = e.detail.token} />
```
## Server Validation
### Cloudflare Workers
```typescript
interface Env {
TURNSTILE_SECRET: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const formData = await request.formData();
const token = formData.get('cf-turnstile-response');
if (!token) {
return new Response('Missing token', { status: 400 });
}
// Validate token
const ip = request.headers.get('CF-Connecting-IP');
const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: env.TURNSTILE_SECRET,
response: token,
remoteip: ip
})
});
const validation = await result.json();
if (!validation.success) {
return new Response('CAPTCHA validation failed', { status: 403 });
}
// Process form...
return new Response('Success');
}
};
```
### Pages Functions
```typescript
// functions/submit.ts - same pattern as Workers, use ctx.env and ctx.request
export const onRequestPost: PagesFunction<{ TURNSTILE_SECRET: string }> = async (ctx) => {
const token = (await ctx.request.formData()).get('cf-turnstile-response');
// Validate with ctx.env.TURNSTILE_SECRET (same as Workers pattern above)
};
```
## Advanced Patterns
### Pre-Clearance (Invisible)
```html
<div id="turnstile-precheck"></div>
<form id="protected-form" style="display: none;">
<button type="submit">Submit</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<script>
let cachedToken = null;
window.onload = () => {
window.turnstile.render('#turnstile-precheck', {
sitekey: 'YOUR_SITE_KEY',
size: 'invisible',
callback: (token) => {
cachedToken = token;
document.getElementById('protected-form').style.display = 'block';
}
});
};
</script>
```
### Token Refresh on Expiry
```javascript
let widgetId = window.turnstile.render('#container', {
sitekey: 'YOUR_SITE_KEY',
'refresh-expired': 'manual',
'expired-callback': () => {
console.log('Token expired, refreshing...');
window.turnstile.reset(widgetId);
}
});
```
## Testing
### Environment-Based Keys
```javascript
const SITE_KEY = process.env.NODE_ENV === 'production'
? 'YOUR_PRODUCTION_SITE_KEY'
: '1x00000000000000000000AA'; // Always passes
const SECRET_KEY = process.env.NODE_ENV === 'production'
? process.env.TURNSTILE_SECRET
: '1x0000000000000000000000000000000AA';
```