alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
192 lines (191 loc) • 5.47 kB
JavaScript
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