alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
136 lines (123 loc) • 3.81 kB
text/typescript
import { $context, AlephaError, z } from "alepha";
import { $logger } from "alepha/logger";
import type { CaptchaProvider } from "./CaptchaProvider.ts";
/**
* Cloudflare Turnstile captcha verification provider.
*
* Validates captcha tokens against the Cloudflare Turnstile siteverify API.
* Free, privacy-friendly, and supports invisible mode.
*
* ## Setup
*
* 1. Create a Turnstile widget at https://dash.cloudflare.com/?to=/:account/turnstile
* 2. Copy the **Site Key** (public, for the client) and **Secret Key** (private, for the server)
* 3. Set `TURNSTILE_SECRET_KEY` in your environment
*
* ## Client-side integration
*
* Add the Turnstile script and widget to your form:
*
* ```html
* <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
* <form>
* <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
* <button type="submit">Submit</button>
* </form>
* ```
*
* The widget injects a hidden `cf-turnstile-response` input into the form.
* Send this value as the `captchaToken` in your registration request.
*
* For explicit rendering (React, SPA):
*
* ```ts
* turnstile.render("#container", {
* sitekey: "YOUR_SITE_KEY",
* callback: (token) => setCaptchaToken(token),
* });
* ```
*
* ## Server-side usage
*
* Register the provider in your app:
*
* ```ts
* import { CaptchaProvider } from "alepha/captcha";
* import { TurnstileCaptchaProvider } from "alepha/captcha";
*
* alepha.with({ provide: CaptchaProvider, use: TurnstileCaptchaProvider });
* ```
*
* ## Test keys (for development)
*
* - Always passes: site `1x00000000000000000000AA`, secret `1x0000000000000000000000000000000AA`
* - Always blocks: site `2x00000000000000000000AB`, secret `2x0000000000000000000000000000000AB`
* - Forces interactive: site `3x00000000000000000000FF`
*
* ## Environment Variables
*
* - `TURNSTILE_SECRET_KEY`: The secret key from the Cloudflare Turnstile dashboard.
*
* @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
*/
export class TurnstileCaptchaProvider implements CaptchaProvider {
protected readonly log = $logger();
protected readonly secretKey: string;
protected readonly siteKey: string;
constructor() {
const { alepha } = $context();
const env = alepha.parseEnv(
z.object({
TURNSTILE_SECRET_KEY: z.text({
description:
"The secret key from the Cloudflare Turnstile dashboard.",
}),
TURNSTILE_SITE_KEY: z.text({
description:
"The public site key from the Cloudflare Turnstile dashboard, rendered on the client.",
}),
}),
);
this.secretKey = env.TURNSTILE_SECRET_KEY;
this.siteKey = env.TURNSTILE_SITE_KEY;
}
public getSiteKey(): string {
return this.siteKey;
}
public async verify(token: string, ip?: string): Promise<boolean> {
const body = new URLSearchParams();
body.set("secret", this.secretKey);
body.set("response", token);
if (ip) {
body.set("remoteip", ip);
}
try {
const res = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
body,
},
);
const data = (await res.json()) as TurnstileResponse;
if (!data.success) {
this.log.debug("Turnstile verification failed", {
errorCodes: data["error-codes"],
});
}
return data.success;
} catch (error) {
throw new AlephaError("Failed to verify Turnstile captcha token", {
cause: error,
});
}
}
}
interface TurnstileResponse {
success: boolean;
"error-codes"?: string[];
challenge_ts?: string;
hostname?: string;
action?: string;
cdata?: string;
}