UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

192 lines (191 loc) 5.47 kB
import { $context, $module, AlephaError, z } from "alepha"; import { $logger } from "alepha/logger"; //#region ../../src/captcha/providers/CaptchaProvider.ts /** * Captcha verification provider interface. * * Verifies that a user-submitted captcha token is valid. Implementations * call the relevant captcha service (Turnstile, reCAPTCHA, hCaptcha, etc.) * to validate the token server-side. */ var CaptchaProvider = class { /** * Public site/widget key to hand to the browser, when applicable. * * Returns `undefined` for providers that don't need a client key * (e.g. in-memory/test providers). */ getSiteKey() {} }; //#endregion //#region ../../src/captcha/providers/MemoryCaptchaProvider.ts /** * In-memory captcha provider for testing. * * Accepts all tokens by default. Use `reject()` to make verification fail, * and `accept()` to restore. All verification attempts are recorded for assertions. */ var MemoryCaptchaProvider = class { log = $logger(); /** * All verification attempts. */ records = []; shouldAccept = true; getSiteKey() {} async verify(token, ip) { this.log.debug("Verifying captcha in memory store", { token, ip }); this.records.push({ token, ip, verifiedAt: /* @__PURE__ */ new Date() }); return this.shouldAccept; } /** * Make all subsequent verifications fail. */ reject() { this.shouldAccept = false; } /** * Make all subsequent verifications pass (default behavior). */ accept() { this.shouldAccept = true; } /** * Whether a token was verified. */ wasVerified(token) { return this.records.some((r) => r.token === token); } /** * Get the last verification attempt. */ get last() { return this.records[this.records.length - 1]; } }; //#endregion //#region ../../src/captcha/providers/TurnstileCaptchaProvider.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/ */ var TurnstileCaptchaProvider = class { log = $logger(); secretKey; siteKey; 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; } getSiteKey() { return this.siteKey; } async verify(token, ip) { const body = new URLSearchParams(); body.set("secret", this.secretKey); body.set("response", token); if (ip) body.set("remoteip", ip); try { const data = await (await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body })).json(); 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 }); } } }; //#endregion //#region ../../src/captcha/index.ts /** * Captcha verification for bot protection. * * **Features:** * - Provider abstraction for captcha services * - Cloudflare Turnstile support (free, privacy-friendly) * - In-memory provider for testing * * @module alepha.captcha */ const AlephaCaptcha = $module({ name: "alepha.captcha", services: [CaptchaProvider], variants: [MemoryCaptchaProvider, TurnstileCaptchaProvider], register: (alepha) => alepha.with({ optional: true, provide: CaptchaProvider, use: MemoryCaptchaProvider }) }); //#endregion export { AlephaCaptcha, CaptchaProvider, MemoryCaptchaProvider, TurnstileCaptchaProvider }; //# sourceMappingURL=index.js.map