import { getClientIp } from "@/lib/auth/rateLimit"; type SiteverifyResult = { success?: unknown; action?: unknown; hostname?: unknown; }; function expectedHostnames(): Set { return new Set( (process.env.TURNSTILE_HOSTNAMES ?? "") .split(",") .map((hostname) => hostname.trim()) .filter(Boolean), ); } export async function verifyTurnstileToken( token: unknown, expectedAction: string, request: Request, ): Promise { const secret = process.env.TURNSTILE_SECRET_KEY; const hostnames = expectedHostnames(); if ( typeof secret !== "string" || secret.length === 0 || typeof token !== "string" || token.length === 0 || token.length > 2048 || hostnames.size === 0 ) { return false; } const body = new URLSearchParams({ secret, response: token }); const remoteIp = getClientIp(request); if (remoteIp !== "unknown") body.set("remoteip", remoteIp); try { const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, signal: AbortSignal.timeout(10_000), body, }); if (!response.ok) return false; const result = (await response.json()) as SiteverifyResult; return result.success === true && result.action === expectedAction && typeof result.hostname === "string" && hostnames.has(result.hostname); } catch { return false; } }