auth-turnstile.ts84 lines · main
1import { log } from '../lib/logger.js';
2import { env } from '../env.js';
3
4/**
5 * Cloudflare Turnstile server-side verification.
6 *
7 * The frontend renders an invisible Turnstile widget and sends the
8 * resulting token as `turnstileToken` on sign-up / sign-in requests.
9 * This module verifies the token against Cloudflare's siteverify endpoint.
10 *
11 * Docs: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
12 */
13
14const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
15
16export interface TurnstileVerifyResult {
17 success: boolean;
18 /** Error codes from Cloudflare when verification fails. */
19 errorCodes?: string[];
20 /** Human-readable message for frontend display. */
21 message?: string;
22}
23
24interface TurnstileResponse {
25 success: boolean;
26 'error-codes'?: string[];
27 challenge_ts?: string;
28 hostname?: string;
29}
30
31/**
32 * Verify a Turnstile token. Returns `{success:true}` when the token is
33 * valid. Returns `{success:false}` with error codes when invalid or when
34 * the secret key is not configured.
35 *
36 * In development (BRIVEN_ENV === 'development'), verification is skipped
37 * and always succeeds if no secret is configured — so local development
38 * doesn't require a Turnstile key pair.
39 */
40export async function verifyTurnstileToken(token: string): Promise<TurnstileVerifyResult> {
41 const secret = env.BRIVEN_TURNSTILE_SECRET_KEY;
42
43 if (!secret) {
44 if (env.BRIVEN_ENV === 'development') {
45 return { success: true };
46 }
47 log.warn('turnstile_secret_missing');
48 return {
49 success: false,
50 errorCodes: ['secret-missing'],
51 message: 'Turnstile is not configured on the server',
52 };
53 }
54
55 try {
56 const response = await fetch(TURNSTILE_VERIFY_URL, {
57 method: 'POST',
58 headers: { 'content-type': 'application/x-www-form-urlencoded' },
59 body: new URLSearchParams({ secret, response: token }),
60 });
61
62 const body = (await response.json()) as TurnstileResponse;
63
64 if (body.success) {
65 return { success: true };
66 }
67
68 const errorCodes = body['error-codes'] ?? ['unknown'];
69 return {
70 success: false,
71 errorCodes,
72 message: `Turnstile verification failed: ${errorCodes.join(', ')}`,
73 };
74 } catch (err) {
75 log.warn('turnstile_verify_error', {
76 message: err instanceof Error ? err.message : String(err),
77 });
78 return {
79 success: false,
80 errorCodes: ['network-error'],
81 message: 'Could not verify Turnstile token. Please try again.',
82 };
83 }
84}