Files
2026-04-16 14:29:07 +10:00

108 lines
2.6 KiB
TypeScript

import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { logger } from '../../utils/logger';
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
type TurnstileVerifyResponse = {
success: boolean;
'error-codes': string[];
challenge_ts?: string;
hostname?: string;
};
/**
* Verify a captcha token server-side.
*
* Currently supports Cloudflare Turnstile. This is a no-op if
* `NEXT_PRIVATE_TURNSTILE_SECRET_KEY` is not configured, making captcha
* verification an opt-in feature.
*/
export const verifyCaptchaToken = async ({
token,
ipAddress,
}: {
token?: string | null;
ipAddress?: string | null;
}) => {
const secretKey = process.env.NEXT_PRIVATE_TURNSTILE_SECRET_KEY;
// If no secret key is configured, skip verification.
if (!secretKey) {
return;
}
if (!token) {
logger.warn({
msg: 'Captcha verification rejected: missing token',
ipAddress,
});
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
message: 'Captcha token is required',
statusCode: 400,
});
}
const formData = new URLSearchParams();
formData.append('secret', secretKey);
formData.append('response', token);
if (ipAddress) {
formData.append('remoteip', ipAddress);
}
let response: Response;
try {
response = await fetch(TURNSTILE_VERIFY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
});
} catch (err) {
logger.error({
msg: 'Captcha verification failed: network error calling siteverify',
err,
ipAddress,
});
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
message: 'Captcha verification failed',
statusCode: 400,
});
}
if (!response.ok) {
logger.error({
msg: 'Captcha verification failed: non-2xx response from siteverify',
status: response.status,
ipAddress,
});
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
message: `Captcha verification request failed with status ${response.status}`,
statusCode: 400,
});
}
const result: TurnstileVerifyResponse = await response.json();
if (!result.success) {
logger.warn({
msg: 'Captcha verification rejected by provider',
errorCodes: result['error-codes'],
hostname: result.hostname,
ipAddress,
});
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
message: `Captcha verification failed: ${result['error-codes']?.join(', ') ?? 'unknown'}`,
statusCode: 400,
});
}
};