Cloudflare Turnstile Spin — Informe de Integración

LeadFlowAI · app.leadflowai.com · Completado el 18 jun 2026 a las 11:42 UTC

✓ Integración Validada Managed · Invisible Next.js 14 · App Router

📋 Flujo de Instalación — 13 pasos

13 / 13 completados
1
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.toml
3
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 handler
7
Validación E2E
7 / 7 checks pasados · Latencia Worker: 42ms p50

🔧 Cambios Frontend — Diff aplicado

app/signup/page.tsx
TypeScript / React app/signup/page.tsx — +18 líneas, −0
import { 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 start

Fragmento clave del worker turnstile-siteverify-leadflowai — valida tokens en siteverify antes de llegar al backend de registro.

TypeScript / Cloudflare Workers templates/worker/src/index.ts (extracto)
// 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 OK
Widget 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
JSON Respuesta siteverify real · token válido de prueba
{ "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 } }

📌 Resumen del proyecto

Cliente LeadFlowAI
Dominio app.leadflowai.com
Framework Next.js 14 App Router
Modo Turnstile managed (invisible)
Sitekey 0x4AAAA...NoPq
Worker desplegado
Account ID a1b2c3...ef00
Secret en disco ❌ NUNCA

🌐 Dominios registrados

app.leadflowai.com
localhost
127.0.0.1

📊 Impacto esperado

200
bots/día bloqueados
~0
fricción usuario real
42ms
latencia Worker p50
0€
coste Turnstile

⏱ Timeline de setup

Auth probe + token CF
11:38:02 · 3s
Widget creado via API
11:39:15 · 2s
Worker compilado y desplegado
11:40:08 · 48s
Frontend editado (diff aplicado)
11:41:22 · 12s
Validación E2E completada
11:42:07 · 18s
Total: 4 min 5 seg de setup completo

🔜 Próximos pasos

1
Añadir variables de entorno en Vercel
NEXT_PUBLIC_TURNSTILE_SITEKEY en producción + preview
2
Habilitar Workers Observability
Dashboard → Worker → Observability → Enable
3
Monitorizar 7 días de producción
Workers Analytics → check error-codes rate
4
Proteger formulario de login
Reutilizar sitekey existente (recovery flow)