New Captcha Package added

This commit is contained in:
2026-07-25 13:38:18 +05:30
parent d0aded0392
commit 8b728a3e5d
157 changed files with 12092 additions and 1913 deletions
+124
View File
@@ -0,0 +1,124 @@
import { copyFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { definePlugin } from "@wrnexus/plugin";
import { CAPTCHA_IMAGE_STYLES } from "./challenges/styles.ts";
export interface CaptchaPluginOptions {
componentDir?: string;
exposeComponentDirectory?: boolean;
enableDevToolbar?: boolean;
auditExternalProviders?: boolean;
}
export interface CaptchaAuditIssue {
id: string;
severity: "error" | "warning" | "suggestion";
title: string;
message: string;
file: string;
}
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const captchaClientRuntime = join(packageRoot, "assets", "client", "captcha.js");
async function copyCaptchaClientRuntime(destinationDir: string): Promise<void> {
await mkdir(destinationDir, { recursive: true });
await copyFile(captchaClientRuntime, join(destinationDir, "captcha.js"));
}
async function installCaptchaClientRuntime(root: string, includeBuildOutput: boolean): Promise<void> {
await copyCaptchaClientRuntime(join(root, "public", "assets", "wrnexus"));
if (includeBuildOutput) {
await copyCaptchaClientRuntime(join(root, "dist", "public", "assets", "wrnexus"));
}
}
export function captchaComponentsDir(): string {
return join(packageRoot, "components");
}
function auditCaptchaSource(code: string, file: string, external: boolean): CaptchaAuditIssue[] {
if (!code.includes("<Captcha") && !code.includes('data-component="Captcha"')) return [];
const issues: CaptchaAuditIssue[] = [];
const push = (id: string, severity: CaptchaAuditIssue["severity"], title: string, message: string) =>
issues.push({ id: `${id}:${file}`, severity, title, message, file });
if (/secret(Key)?\s*=|providerSecret\s*=|captchaSecret\s*=/i.test(code)) {
push("client-secret", "error", "CAPTCHA secret exposed", "Never pass a provider secret or verification secret to a .wrn component.");
}
if (!/action\s*=/.test(code)) {
push("missing-action", "warning", "CAPTCHA action is missing", "Bind each challenge to a stable action such as signup, login, or contact-submit.");
}
if (/required\s*=\s*["']?false/i.test(code)) {
push("optional-captcha", "warning", "CAPTCHA is optional", "Protected forms should require a CAPTCHA response and verify it on the server.");
}
if (!/showAudio\s*=|presentation\s*=\s*["']audio/i.test(code)) {
push("audio-alternative", "suggestion", "Confirm an accessible alternative", "Visual challenges should offer an audio or non-visual alternative.");
}
const imageStyle = code.match(/imageStyle\s*=\s*["']([^"']+)["']/i)?.[1]?.trim().toLowerCase();
if (imageStyle && !CAPTCHA_IMAGE_STYLES.includes(imageStyle as (typeof CAPTCHA_IMAGE_STYLES)[number])) {
push(
"unknown-image-style",
"error",
"Unknown CAPTCHA image style",
`Use one of: ${CAPTCHA_IMAGE_STYLES.join(", ")}.`,
);
}
const disturbance = Number(code.match(/disturbance\s*=\s*["']?(\d+)/i)?.[1] ?? "");
if (Number.isFinite(disturbance) && disturbance >= 65 && /showAudio\s*=\s*["']?false/i.test(code)) {
push(
"hard-without-audio",
"warning",
"Hard CAPTCHA has no audio alternative",
"High disturbance should include audio or another non-visual challenge path.",
);
}
if (external && /provider\s*=\s*["'](?:turnstile|recaptcha|hcaptcha)/i.test(code) && !/siteKey\s*=/.test(code)) {
push("missing-site-key", "error", "Provider site key is missing", "External CAPTCHA providers require a public site key in the browser.");
}
push("server-verification", "suggestion", "Server verification required", "Confirm the receiving API uses captchaGuard(), parseWithCaptcha(), or provider.verify().");
return issues;
}
export function captchaPlugin(options: CaptchaPluginOptions = {}) {
const metadataKey = "@wrnexus/captcha:audit";
return definePlugin({
name: "@wrnexus/captcha",
version: "0.3.6",
enforce: "post",
async configure(config, context) {
const current = (config.captcha ?? {}) as Record<string, unknown>;
config.captcha = {
componentDir: options.componentDir ?? captchaComponentsDir(),
...current,
};
context.metadata.set("@wrnexus/captcha:component-dir", options.componentDir ?? captchaComponentsDir());
await installCaptchaClientRuntime(context.root, context.command === "build");
},
transformCode(code, context) {
if (context.mode !== "development") return;
const previous = (context.metadata.get(metadataKey) as CaptchaAuditIssue[] | undefined) ?? [];
const withoutFile = previous.filter((issue) => issue.file !== context.file);
context.metadata.set(
metadataKey,
[...withoutFile, ...auditCaptchaSource(code, context.file, options.auditExternalProviders ?? true)],
);
},
devToolbarPanels(context) {
if (options.enableDevToolbar === false) return [];
const issues = (context.metadata.get(metadataKey) as CaptchaAuditIssue[] | undefined) ?? [];
return [{
id: "wrnexus-captcha",
title: "CAPTCHA",
icon: "shield-check",
badge: issues.length,
description: "CAPTCHA security, accessibility, and integration checks",
issues,
}];
},
});
}
export default captchaPlugin;