📋 Flujo de Instalación — 13 pasos
13 / 13 completados1
Auth probe
Token verificado con permisos Account.Turnstile:Edit + Account.Workers Scripts:Edit
{"status":"ok","account_id":"a1b2c3d4e5f6789012345678abcdef00","account_name":"LeadFlowAI","token_preview":"CFvY...xQ9"}
2
Dominios registrados
localhost, 127.0.0.1, app.leadflowai.com detectados de
wrangler.toml3
Escaneo de codebase
Framework: Next.js 14 App Router. No se detectó reCAPTCHA ni hCaptcha previo.
4
Widget creado
Modo: managed · 3 dominios registrados · Secret almacenado en env (nunca en disco)
{"status":"ok","sitekey":"0x4AAAAAABcDeFgHiJkLmNoPq","name":"leadflowai-signup","mode":"managed","domains":["app.leadflowai.com","localhost","127.0.0.1"]}
5
Worker desplegado
turnstile-siteverify-leadflowai · TURNSTILE_SECRET_KEY inyectado vía stdin (sin tocar disco)
{"status":"ok","worker_name":"turnstile-siteverify-leadflowai","worker_url":"https://turnstile-siteverify-leadflowai.workers.dev","region":"auto"}
6
Frontend editado
Insertados: script tag Turnstile, widget
cf-turnstile, gate en submit handler7
Validación E2E
7 / 7 checks pasados · Latencia Worker: 42ms p50
🔧 Cambios Frontend — Diff aplicado
app/signup/page.tsximport { useState } from 'react'
import Script from 'next/script'
const WORKER_URL = 'https://turnstile-siteverify-leadflowai.workers.dev'
export default function SignupPage() {'{'}
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {'{'}
e.preventDefault()
const form = e.currentTarget
const token = (form.elements.namedItem('cf-turnstile-response') as HTMLInputElement)?.value
const res = await fetch(WORKER_URL, {'{'}
method: 'POST',
headers: {'{'} 'Content-Type': 'application/json' {'}'},
body: JSON.stringify({'{'} token {'}'})
{'}'})
const data = await res.json()
if (!data.success) {'{'} setError('Verificación fallida. Inténtalo de nuevo.'); return {'}'}
// Existing handler logic — unchanged
await registerUser(new FormData(form))
{'}'}
return (
<>
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" strategy="afterInteractive" />
<form onSubmit={handleSubmit}>
<input name="name" ... />
<input name="email" ... />
<input name="password" ... />
<div
className="cf-turnstile"
data-sitekey="0x4AAAAAABcDeFgHiJkLmNoPq"
data-action="turnstile-spin-v1"
data-appearance="interaction-only"
/>
<button type="submit">Crear cuenta</button>
</form>
</>
)
{'}'}
El handler existente
registerUser() no fue modificado. Turnstile solo añade el gate de validación antes de que corra — si data.success === false el signup se detiene. El diseño del formulario permanece intacto.☁️ Worker Desplegado — Cloudflare Workers
v1.0.0 · 2 ms cold startFragmento clave del worker turnstile-siteverify-leadflowai — valida tokens en siteverify antes de llegar al backend de registro.
// POST / — recibe {token} del frontend, valida contra siteverify
async function handleSiteverify(request: Request, env: Env): Promise<Response> {
const parsed = await parseBody(request) // JSON o form-urlencoded
// CF-Connecting-IP como fallback si no viene remoteip
const remoteip = parsed.remoteip
?? request.headers.get('CF-Connecting-IP')
?? undefined
const { response, durationMs } = await validate(
{ ...parsed, remoteip },
env,
WORKER_VERSION
)
// Siempre HTTP 200 — el caller debe leer response.success
return jsonResponse(response, 200, env)
}
// GET /health — health check
handleHealth(env): Response => jsonResponse({
ok: true,
version: '1.0.0',
service: 'turnstile-siteverify',
docs: 'https://developers.cloudflare.com/turnstile/spin/'
}, 200, env)
✅ Validación E2E — Scripts/validate.sh
7 / 7 checks OKWidget API responde 200
Worker /health → ok: true
Token válido → success: true
Token inválido → success: false
Token vacío → error: missing-input-response
CORS headers presentes
Sitekey matchea widget registrado
Slot libre
{
"success": true,
"challenge_ts": "2026-06-18T11:42:07.831Z",
"hostname": "app.leadflowai.com",
"action": "turnstile-spin-v1",
"cdata": "",
"error-codes": [],
"_spin_meta": {
"worker_version": "1.0.0",
"duration_ms": 42
}
}