Files
WRNexusJSDoc/app/pages/packages/captcha.wrn
T

322 lines
26 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
page wrnexuscaptcha {
seo {
title = "@wrnexus/captcha"
description = "Managed CAPTCHA verification, middleware, and UI integration."
}
view {
<div class="docs-shell">
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
<header class="topbar">
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.5.11</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
</header>
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
<main class="portal-main docs-layout">
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/captcha</span></nav><section class="doc-intro"><span class="eyebrow">Security · Package reference</span><h1>@wrnexus/captcha</h1><p>Managed CAPTCHA verification, middleware, and UI integration.</p><div class="doc-meta"><span>v0.5.11</span><span>Private registry</span><span>Security</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/captcha@0.5.11</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide"><p>A first-class CAPTCHA and anti-automation package for WRNexusJS. It supports self-hosted challenges, a managed WRNexus service, external providers, form submission guards, page gates, accessible audio, adaptive risk checks, and a Tailwind-only <code>.wrn</code> component.</p>
<h3 id="install">Install</h3>
<pre data-language="bash"><code>bun add @wrnexus/captcha</code></pre>
<p>WRNexusJS automatically discovers the package plugin, component, client runtime, styles, and DevToolbar audit. Use <code>&lt;Captcha /&gt;</code> directly after installation. The browser runtime is injected once only on responses that render a CAPTCHA; no script tag, public-file copy, or manual plugin registration is required. Call <code>captchaPlugin(options)</code> explicitly only when an application needs to override the discovered package configuration.</p>
<h3 id="included-challenge-modes">Included challenge modes</h3>
<ul>
<li>Number, alphabet, and alphanumeric image challenges</li>
<li>Addition, subtraction, multiplication, and exact-division calculations</li>
<li>Generated shape-selection image challenges</li>
<li>Audio alternatives for text, numbers, and calculations</li>
<li>Honeypot and minimum-completion-time invisible checks</li>
<li>Self-hosted “Im not a robot” checkbox challenge with one-time server verification</li>
<li>Always, once-per-session, and adaptive page gates</li>
<li>Cloudflare Turnstile, Google reCAPTCHA, hCaptcha, managed, and custom providers</li>
</ul>
<h3 id="create-the-self-hosted-engine">Create the self-hosted engine</h3>
<pre data-language="ts"><code>import &#123;
createCaptchaEngine,
createCaptchaHttpHandlers,
RedisCaptchaStore,
&#125; from &quot;@wrnexus/captcha/server&quot;;
const engine = createCaptchaEngine(&#123;
secret: process.env.CAPTCHA_SECRET!,
store: new RedisCaptchaStore(redis),
basePath: &quot;/api/captcha&quot;,
challengeTtlMs: 2 * 60_000,
responseTokenTtlMs: 5 * 60_000,
maxAttempts: 3,
minCompletionMs: 800,
&#125;);
export const handlers = createCaptchaHttpHandlers(engine);</code></pre>
<p>Mount the handlers from an API catch-all route:</p>
<pre data-language="ts"><code>import type &#123; Context &#125; from &quot;@wrnexus/core&quot;;
import &#123; handlers &#125; from &quot;../../lib/captcha.ts&quot;;
export async function POST(ctx: Context) &#123;
return (await handlers.handle(ctx.req, ctx)) ?? new Response(&quot;Not Found&quot;, &#123; status: 404 &#125;);
&#125;
export const GET = POST;
export const HEAD = POST;</code></pre>
<h3 id="use-the-component">Use the component</h3>
<pre data-language="wrn"><code>&lt;Captcha
type=&quot;alphanumeric&quot;
action=&quot;signup&quot;
endpoint=&quot;/api/captcha/challenge&quot;
verifyEndpoint=&quot;/api/captcha/verify&quot;
difficulty=&quot;normal&quot;
disturbance=&quot;50&quot;
imageStyle=&quot;random&quot;
allowedStyles=&quot;classic,snow,distortion,wave&quot;
size=&quot;normal&quot;
showAudio=&quot;true&quot;
showListen=&quot;true&quot;
@success='captchaToken = event.detail.responseToken'
@failure='formError = event.detail.extra.message'
/&gt;</code></pre>
<p>The component uses Tailwind utilities and <code>--wire-*</code> theme variables. It has no companion component CSS file.</p>
<h4 id="main-props">Main props</h4>
<p><code>provider</code>, <code>siteKey</code>, <code>type</code>, <code>action</code>, <code>presentation</code>, <code>difficulty</code>, <code>disturbance</code>, <code>imageStyle</code>, <code>allowedStyles</code>, <code>excludedStyles</code>, <code>randomizeStyle</code>, <code>locale</code>, <code>size</code>, <code>color</code>, <code>class</code>, <code>name</code>, <code>endpoint</code>, <code>verifyEndpoint</code>, <code>responseField</code>, labels/messages, <code>autoLoad</code>, <code>autoVerify</code>, <code>showVerify</code>, <code>showRefresh</code>, <code>showAudio</code>, <code>showListen</code>, <code>showStatus</code>, <code>disabled</code>, <code>required</code>, and the backward-compatible <code>compact</code> alias.</p>
<h4 id="component-sizes">Component sizes</h4>
<p>Use one of the three supported display modes:</p>
<pre data-language="wrn"><code>&lt;Captcha size=&quot;compact&quot; action=&quot;small-form&quot; /&gt;
&lt;Captcha size=&quot;normal&quot; action=&quot;standard-form&quot; /&gt;
&lt;Captcha size=&quot;big&quot; action=&quot;security-page&quot; /&gt;</code></pre>
<p><code>small</code>/<code>sm</code> are accepted as aliases for <code>compact</code>, while <code>large</code>/<code>lg</code> are accepted as aliases for <code>big</code>. The old <code>compact=&quot;true&quot;</code> prop still forces compact mode.</p>
<h4 id="listen-button-visibility">Listen button visibility</h4>
<p>Audio remains available by default. Hide the Listen and Use audio controls with either of these props:</p>
<pre data-language="wrn"><code>&lt;Captcha showListen=&quot;false&quot; action=&quot;without-listen-button&quot; /&gt;
&lt;Captcha showAudio=&quot;false&quot; action=&quot;without-audio-alternative&quot; /&gt;</code></pre>
<p><code>showListen</code> is the direct UI switch. <code>showAudio</code> remains the broader backward-compatible audio switch.</p>
<h4 id="i-m-not-a-robot-checkbox">Im not a robot checkbox</h4>
<pre data-language="wrn"><code>&lt;Captcha
type=&quot;not-robot&quot;
action=&quot;contact-submit&quot;
size=&quot;compact&quot;
showListen=&quot;false&quot;
/&gt;</code></pre>
<p>The checkbox is not a client-only boolean. Clicking it completes a self-hosted invisible challenge that is time-limited, attempt-limited, one-time-use, action-bound, optionally session/hostname/IP-bound, and verified on the server. It is a low-friction anti-automation layer; use adaptive escalation to a visual or external provider for high-risk traffic.</p>
<h4 id="visual-disturbance">Visual disturbance</h4>
<p>Use <code>disturbance</code> for visual and image-selection challenges. It accepts an integer from <code>25</code> through <code>75</code>:</p>
<ul>
<li><code>25</code>: light disturbance and easiest readability</li>
<li><code>50</code>: balanced default</li>
<li><code>75</code>: maximum supported dots, line crossings, glyph movement, and image-tile noise</li>
</ul>
<p>The browser sends this value to the challenge API, and the server validates the range before generating the challenge. It is also returned in challenge metadata.</p>
<h4 id="generated-image-renderer-styles">Generated image renderer styles</h4>
<p>Text, number, alphanumeric, and calculation CAPTCHA images support 18 concrete renderers plus a random mode:</p>
<p><code>classic</code>, <code>collision</code>, <code>snow</code>, <code>corrosion</code>, <code>spiderweb</code>, <code>cross-shadow</code>, <code>split</code>, <code>split2</code>, <code>cut</code>, <code>darts</code>, <code>distortion</code>, <code>stitch</code>, <code>striped</code>, <code>wave</code>, <code>grid-noise</code>, <code>scribble</code>, <code>pixel</code>, and <code>broken-lines</code>.</p>
<p>Use a fixed style:</p>
<pre data-language="wrn"><code>&lt;Captcha
type=&quot;alphanumeric&quot;
action=&quot;signup&quot;
imageStyle=&quot;spiderweb&quot;
disturbance=&quot;55&quot;
/&gt;</code></pre>
<p>Use a new random style whenever the challenge is refreshed:</p>
<pre data-language="wrn"><code>&lt;Captcha
type=&quot;number&quot;
action=&quot;login&quot;
imageStyle=&quot;random&quot;
difficulty=&quot;normal&quot;
/&gt;</code></pre>
<p>Control the random pool with comma-separated component props or arrays in the TypeScript API:</p>
<pre data-language="wrn"><code>&lt;Captcha
type=&quot;alphanumeric&quot;
action=&quot;checkout&quot;
imageStyle=&quot;random&quot;
allowedStyles=&quot;classic,snow,distortion,wave&quot;
excludedStyles=&quot;collision&quot;
/&gt;</code></pre>
<pre data-language="ts"><code>const challenge = await engine.create(&#123;
action: &quot;checkout&quot;,
type: &quot;alphanumeric&quot;,
imageStyle: &quot;random&quot;,
allowedStyles: [&quot;classic&quot;, &quot;snow&quot;, &quot;distortion&quot;, &quot;wave&quot;],
excludedStyles: [&quot;collision&quot;],
&#125;);</code></pre>
<p>Set <code>randomizeStyle: true</code> to force random selection even when <code>imageStyle</code> names a concrete renderer. The resolved style, requested style, and active pool are returned in challenge metadata. The answer is never embedded in metadata or browser JavaScript.</p>
<h4 id="events">Events</h4>
<p><code>@ready</code>, <code>@challenge</code>, <code>@input</code>, <code>@verify</code>, <code>@success</code>, <code>@failure</code>, <code>@expired</code>, <code>@refresh</code>, <code>@audioStart</code>, <code>@audioEnd</code>, and <code>@error</code>.</p>
<h3 id="protect-a-validated-form-api">Protect a validated form API</h3>
<p>Validate a cloned request first, then consume the CAPTCHA response token. This prevents a valid token from being consumed when ordinary field validation fails.</p>
<pre data-language="ts"><code>import &#123; captchaGuard &#125; from &quot;@wrnexus/captcha/server&quot;;
import &#123; parseBody &#125; from &quot;@wrnexus/validation&quot;;
import contactSchema from &quot;../schemas/contact.ts&quot;;
import &#123; engine &#125; from &quot;../lib/captcha.ts&quot;;
const guard = captchaGuard(&#123;
action: &quot;contact-submit&quot;,
engine,
bindHostname: true,
bindSession: true,
&#125;);
export async function POST(ctx) &#123;
const validation = await parseBody(contactSchema, ctx.req.clone());
if (!validation.ok) return validation.response;
return guard(ctx, async () =&gt; Response.json(&#123; ok: true, submission: validation.value &#125;));
&#125;</code></pre>
<p>The CAPTCHA runtime binds its required-form check in the capture phase, so a <code>data-schema</code> validator cannot submit the form before CAPTCHA verification. After a successful form request, the component automatically creates a fresh challenge.</p>
<h4 id="retryable-operations-such-as-login">Retryable operations such as login</h4>
<p>A login may consume a valid CAPTCHA and then fail because the password is incorrect. Configure a short action-bound session grant so the user can correct their credentials without solving CAPTCHA again:</p>
<pre data-language="ts"><code>const guard = captchaGuard(&#123;
action: &quot;auth-login&quot;,
engine,
bindHostname: true,
bindSession: true,
verifiedForMs: 5 * 60_000,
&#125;);</code></pre>
<p>Keep the verified widget state for non-CAPTCHA form errors:</p>
<pre data-language="wrn"><code>&lt;Captcha
action=&quot;auth-login&quot;
required=&quot;true&quot;
resetOnError=&quot;false&quot;
/&gt;</code></pre>
<p>The grant is stored in the current session and bound to the configured action. Expired grants and CAPTCHA-specific errors still require and load a fresh challenge. Keep login rate limits and authentication lockout enabled; <code>verifiedForMs</code> removes repeated human verification, not credential-abuse controls.</p>
<h3 id="validate-a-schema-and-captcha-together">Validate a schema and CAPTCHA together</h3>
<pre data-language="ts"><code>const result = await parseWithCaptcha(signupSchema, body, ctx, &#123;
action: &quot;signup&quot;,
engine,
&#125;);
if (!result.ok) return Response.json(&#123; ok: false, errors: result.errors &#125;, &#123; status: 400 &#125;);</code></pre>
<h3 id="page-gate">Page gate</h3>
<pre data-language="ts"><code>export default captchaPageGate(&#123;
action: &quot;reports-access&quot;,
engine,
challengePath: &quot;/captcha&quot;,
policy: &#123;
mode: &quot;session&quot;,
verifiedForMs: 15 * 60_000,
routeGroups: [&quot;/reports&quot;],
&#125;,
&#125;);</code></pre>
<p>Use <code>mode: &quot;always&quot;</code> for every visit, <code>mode: &quot;session&quot;</code> for a temporary grant, or <code>mode: &quot;adaptive&quot;</code> with <code>signals(ctx)</code>.</p>
<p>The challenge page should post the return path as a normal hidden field instead of constructing JavaScript inside the HTML <code>action</code> attribute:</p>
<pre data-language="wrn"><code>&lt;form method=&quot;post&quot; action=&quot;/api/page-grant&quot;&gt;
&lt;input type=&quot;hidden&quot; name=&quot;returnTo&quot; value='&#123;returnTo&#125;' /&gt;
&lt;Captcha action=&quot;reports-access&quot; /&gt;
&lt;button type=&quot;submit&quot;&gt;Continue&lt;/button&gt;
&lt;/form&gt;</code></pre>
<p>The <code>/api/page-grant</code> route reads <code>returnTo</code>, restricts it to the current origin, and redirects only after <code>captchaPageGate()</code> has verified and stored the temporary session grant.</p>
<h3 id="external-providers">External providers</h3>
<pre data-language="ts"><code>const turnstile = turnstileProvider(&#123;
secretKey: process.env.TURNSTILE_SECRET!,
siteKey: process.env.PUBLIC_TURNSTILE_SITE_KEY!,
expectedHostnames: [&quot;example.com&quot;],
expectedAction: &quot;signup&quot;,
&#125;);</code></pre>
<pre data-language="wrn"><code>&lt;Captcha
provider=&quot;turnstile&quot;
siteKey=&quot;PUBLIC_SITE_KEY&quot;
action=&quot;signup&quot;
/&gt;</code></pre>
<p>Use the matching provider in <code>captchaGuard(&#123; provider: turnstile &#125;)</code>. reCAPTCHA and hCaptcha adapters follow the same pattern.</p>
<h3 id="managed-provider">Managed provider</h3>
<pre data-language="ts"><code>const managed = managedCaptchaProvider(&#123;
baseUrl: &quot;https://captcha.example.com&quot;,
siteKey: process.env.PUBLIC_CAPTCHA_SITE_KEY!,
secretKey: process.env.CAPTCHA_SECRET_KEY!,
&#125;);</code></pre>
<p>For direct browser challenge creation, configure the components <code>endpoint</code> as the managed <code>/v1/challenges</code> URL and its <code>verifyEndpoint</code> as <code>/v1/solve</code>. Keep the secret key only in the server provider.</p>
<h3 id="stores">Stores</h3>
<ul>
<li><code>MemoryCaptchaStore</code>: development and one-process applications</li>
<li><code>SqliteCaptchaStore</code>: adapter for SQLite-like <code>prepare().run/get/all()</code> clients</li>
<li><code>RedisCaptchaStore</code>: shared TTL storage with Lua-backed atomic consumption when <code>eval</code> is available</li>
<li><code>CaptchaStore</code>: implement this interface for PostgreSQL, MySQL, MongoDB, or another backend</li>
</ul>
<h3 id="audio">Audio</h3>
<p><code>AssetAudioRenderer</code> concatenates bundled English PCM WAV clips without calling an external service. Supply a custom <code>CaptchaAudioRenderer</code> for recorded voices, Hindi or other languages, or managed text-to-speech.</p>
<h3 id="devtoolbar">DevToolbar</h3>
<p>The automatically discovered CAPTCHA plugin registers its DevToolbar audit panel. It checks for likely client-side secrets, missing action bindings, missing provider site keys, optional CAPTCHA fields, accessible alternatives, and server-verification reminders. Explicit <code>captchaPlugin(options)</code> registration is needed only to override automatic configuration.</p>
<h3 id="testing">Testing</h3>
<p>Use deterministic custom generators in unit tests. Never require users or CI to solve random CAPTCHA images. The package includes engine, provider, policy, HTTP, storage, replay, expiry, binding, and audio authorization tests.</p>
<h3 id="custom-challenge-generator">Custom challenge generator</h3>
<pre data-language="ts"><code>import &#123; defineCaptchaGenerator, createCaptchaEngine &#125; from &quot;@wrnexus/captcha&quot;;
const wordChallenge = defineCaptchaGenerator(&#123;
type: &quot;word&quot; as const,
generate(context) &#123;
const answer = &quot;NEXUS&quot;;
return &#123;
type: &quot;word&quot;,
presentation: &quot;visual&quot;,
prompt: &quot;Enter the displayed word&quot;,
answer,
answerKind: &quot;text&quot;,
image: renderYourImage(answer),
inputMode: &quot;text&quot;,
&#125;;
&#125;,
&#125;);
const engine = createCaptchaEngine(&#123; secret, generators: [wordChallenge] &#125;);</code></pre>
<p>Applications may also implement <code>CaptchaStore</code>, <code>CaptchaAudioRenderer</code>, or use <code>defineCaptchaProvider()</code> for a completely custom service.</p></section><section id="api" class="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>export &#123; CaptchaAudioRenderer, CaptchaBinding, CaptchaChallenge, CaptchaChallengeGenerator, CaptchaChallengeRecord, CaptchaChallengeType, CaptchaConcreteImageStyle, CaptchaDifficulty, CaptchaEngine, CaptchaEngineOptions, CaptchaFailureCode, CaptchaGeneratorContext, CaptchaGuardOptions, CaptchaHttpHandlers, CaptchaImageItem, CaptchaImageStyle, CaptchaMiddleware, CaptchaPageGateOptions, CaptchaPolicyMode, CaptchaPolicyOptions, CaptchaPresentation, CaptchaProvider, CaptchaProviderClientConfig, CaptchaProviderName, CaptchaResponseTokenRecord, CaptchaRiskResult, CaptchaRiskSignals, CaptchaStore, CaptchaVerificationResult, CreateCaptchaOptions, GeneratedCaptchaChallenge, VerifyCaptchaInput &#125; from './types.js';
export &#123; CaptchaHttpOptions, CaptchaParseResult, CaptchaSessionGrant, DefaultCaptchaEngine, ParseWithCaptchaOptions, bindingHash, bytesToBase64Url, captchaGuard, captchaPageGate, clearCaptchaGrants, constantTimeEqual, createCaptchaEngine, createCaptchaHttpHandlers, defaultRandomBytes, evaluateCaptchaRisk, hmacSha256, parseWithCaptcha, randomId, sha256, shouldRequireCaptcha, validCaptchaGrant &#125; from './server/index.js';
export &#123; CaptchaAuditIssue, CaptchaPluginOptions, captchaComponentsDir, captchaPlugin &#125; from './plugin.js';
export &#123; MemoryCaptchaStore, MemoryCaptchaStoreOptions, createMemoryCaptchaStore &#125; from './stores/memory.js';
export &#123; SqliteCaptchaStore, SqliteCaptchaStoreOptions, SqliteDatabaseLike, SqliteStatementLike, createSqliteCaptchaStore &#125; from './stores/sqlite.js';
export &#123; RedisCaptchaClient, RedisCaptchaStore, RedisCaptchaStoreOptions, createRedisCaptchaStore &#125; from './stores/redis.js';
export &#123; SelfHostedCaptchaProvider, selfHostedProvider &#125; from './providers/self-hosted.js';
export &#123; S as SiteverifyCaptchaProvider, a as SiteverifyPreset, b as SiteverifyProviderOptions &#125; from './siteverify-Cg3TTAp4.js';
export &#123; TurnstileCaptchaProvider, turnstileProvider &#125; from './providers/turnstile.js';
export &#123; RecaptchaProvider, recaptchaProvider &#125; from './providers/recaptcha.js';
export &#123; HcaptchaProvider, hcaptchaProvider &#125; from './providers/hcaptcha.js';
export &#123; ManagedCaptchaProvider, ManagedCaptchaProviderOptions, managedCaptchaProvider &#125; from './providers/managed.js';
export &#123; defineCaptchaProvider &#125; from './providers/custom.js';
export &#123; CAPTCHA_CONCRETE_IMAGE_STYLES, CAPTCHA_IMAGE_STYLES, CalculationCaptchaGenerator, ImageCaptchaGenerator, InvisibleCaptchaGenerator, ResolveCaptchaImageStyleOptions, ResolvedCaptchaImageStyle, Rgba, RgbaImage, TextCaptchaGenerator, alphaCaptchaGenerator, alphanumericCaptchaGenerator, bytesToBase64, calculationCaptchaGenerator, createImage, defaultCaptchaGenerators, defineCaptchaGenerator, drawGlyph, drawLine, drawText, encodePng, fillCircle, fillPolygon, fillRect, honeypotCaptchaGenerator, imageCaptchaGenerator, isCaptchaImageStyle, normalizeCaptchaImageStyle, normalizeCaptchaImageStyleList, notRobotCaptchaGenerator, numberCaptchaGenerator, pngDataUri, resolveCaptchaImageStyle, setPixel, timingCaptchaGenerator &#125; from './challenges/index.js';
export &#123; AssetAudioRenderer, AssetAudioRendererOptions, createAssetAudioRenderer, resolveCaptchaAudioAssetsDir &#125; from './audio/index.js';
import '@wrnexus/core';
import '@wrnexus/validation';
import '@wrnexus/plugin';
</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid"><article class="example-card"><h3>## Install</h3><pre data-language="bash"><code>bun add @wrnexus/captcha</code></pre></article><article class="example-card"><h3>## Create the self-hosted engine</h3><pre data-language="ts"><code>import &#123;
createCaptchaEngine,
createCaptchaHttpHandlers,
RedisCaptchaStore,
&#125; from &quot;@wrnexus/captcha/server&quot;;
const engine = createCaptchaEngine(&#123;
secret: process.env.CAPTCHA_SECRET!,
store: new RedisCaptchaStore(redis),
basePath: &quot;/api/captcha&quot;,
challengeTtlMs: 2 * 60_000,
responseTokenTtlMs: 5 * 60_000,
maxAttempts: 3,
minCompletionMs: 800,
&#125;);
export const handlers = createCaptchaHttpHandlers(engine);</code></pre></article><article class="example-card"><h3>Mount the handlers from an API catch-all route</h3><pre data-language="ts"><code>import type &#123; Context &#125; from &quot;@wrnexus/core&quot;;
import &#123; handlers &#125; from &quot;../../lib/captcha.ts&quot;;
export async function POST(ctx: Context) &#123;
return (await handlers.handle(ctx.req, ctx)) ?? new Response(&quot;Not Found&quot;, &#123; status: 404 &#125;);
&#125;
export const GET = POST;
export const HEAD = POST;</code></pre></article><article class="example-card"><h3>## Use the component</h3><pre data-language="wrn"><code>&lt;Captcha
type=&quot;alphanumeric&quot;
action=&quot;signup&quot;
endpoint=&quot;/api/captcha/challenge&quot;
verifyEndpoint=&quot;/api/captcha/verify&quot;
difficulty=&quot;normal&quot;
disturbance=&quot;50&quot;
imageStyle=&quot;random&quot;
allowedStyles=&quot;classic,snow,distortion,wave&quot;
size=&quot;normal&quot;
showAudio=&quot;true&quot;
showListen=&quot;true&quot;
@success='captchaToken = event.detail.responseToken'
@failure='formError = event.detail.extra.message'
/&gt;</code></pre></article><article class="example-card"><h3>Component sizes</h3><pre data-language="wrn"><code>&lt;Captcha size=&quot;compact&quot; action=&quot;small-form&quot; /&gt;
&lt;Captcha size=&quot;normal&quot; action=&quot;standard-form&quot; /&gt;
&lt;Captcha size=&quot;big&quot; action=&quot;security-page&quot; /&gt;</code></pre></article><article class="example-card"><h3>Listen button visibility</h3><pre data-language="wrn"><code>&lt;Captcha showListen=&quot;false&quot; action=&quot;without-listen-button&quot; /&gt;
&lt;Captcha showAudio=&quot;false&quot; action=&quot;without-audio-alternative&quot; /&gt;</code></pre></article></div></section></article>
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#install">Install</a><a class="toc-level-3" href="#included-challenge-modes">Included challenge modes</a><a class="toc-level-3" href="#create-the-self-hosted-engine">Create the self-hosted engine</a><a class="toc-level-3" href="#use-the-component">Use the component</a><a class="toc-level-4" href="#main-props">Main props</a><a class="toc-level-4" href="#component-sizes">Component sizes</a><a class="toc-level-4" href="#listen-button-visibility">Listen button visibility</a><a class="toc-level-4" href="#i-m-not-a-robot-checkbox">Im not a robot checkbox</a><a class="toc-level-4" href="#visual-disturbance">Visual disturbance</a><a class="toc-level-4" href="#generated-image-renderer-styles">Generated image renderer styles</a><a class="toc-level-4" href="#events">Events</a><a class="toc-level-3" href="#protect-a-validated-form-api">Protect a validated form API</a><a class="toc-level-4" href="#retryable-operations-such-as-login">Retryable operations such as login</a><a class="toc-level-3" href="#validate-a-schema-and-captcha-together">Validate a schema and CAPTCHA together</a><a class="toc-level-3" href="#page-gate">Page gate</a><a class="toc-level-3" href="#external-providers">External providers</a><a class="toc-level-3" href="#managed-provider">Managed provider</a><a class="toc-level-3" href="#stores">Stores</a><a class="toc-level-3" href="#audio">Audio</a><a class="toc-level-3" href="#devtoolbar">DevToolbar</a><a class="toc-level-3" href="#testing">Testing</a><a class="toc-level-3" href="#custom-challenge-generator">Custom challenge generator</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
</main>
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS 0.5.11</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
<BackToTop />
</div>
}
}