const VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" /** * Verifies a Cloudflare Turnstile token server-side against the siteverify API. * * Fails CLOSED when Turnstile is configured (secret present) but the token is * missing or invalid. Fails OPEN only when `TURNSTILE_SECRET_KEY` is unset — so * environments that haven't configured Turnstile keep working, matching how the * other optional integrations (Stripe / OpenAI / SMTP email) degrade in this app. */ export async function verifyTurnstile( token: string | undefined | null, remoteIp?: string | null ): Promise { const secret = process.env.TURNSTILE_SECRET_KEY if (!secret) { // Fail open ONLY outside production. In production a missing secret is a // misconfiguration, not a deployment choice: silently dropping bot // protection from login / signup / forgot-password is worse than a loud // failure, so refuse the request and log once per occurrence. if (process.env.NODE_ENV === "production") { console.error( "[turnstile] TURNSTILE_SECRET_KEY is not set in production — " + "rejecting the request rather than silently disabling bot protection." ) return false } return true // integration disabled in dev — do not block auth } if (!token) return false try { const body = new URLSearchParams() body.append("secret", secret) body.append("response", token) if (remoteIp) body.append("remoteip", remoteIp) const res = await fetch(VERIFY_URL, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body, cache: "no-store", }) const data = (await res.json()) as { success?: boolean } return data.success === true } catch { // Network / provider error — fail closed so a challenge can't be bypassed. return false } }