91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import type { Context } from "@wrnexus/core";
|
|
import type {
|
|
CaptchaEngine,
|
|
CaptchaProvider,
|
|
CaptchaVerificationResult,
|
|
VerifyCaptchaInput,
|
|
} from "./types.ts";
|
|
import { selfHostedProvider } from "./providers/self-hosted.ts";
|
|
|
|
export function captchaTokenFrom(
|
|
value: Request | Headers | FormData | URLSearchParams | Record<string, unknown>,
|
|
field = "wrn-captcha-response",
|
|
): Promise<string | undefined> | string | undefined {
|
|
if (value instanceof Request) {
|
|
const header = value.headers.get("x-wrn-captcha-token");
|
|
if (header) return header;
|
|
return value
|
|
.clone()
|
|
.formData()
|
|
.then((form) => {
|
|
const token = form.get(field) ?? form.get("captchaToken") ?? form.get("responseToken");
|
|
return typeof token === "string" ? token : undefined;
|
|
})
|
|
.catch(async () => {
|
|
try {
|
|
const body = (await value.clone().json()) as Record<string, unknown>;
|
|
const token = body[field] ?? body.captchaToken ?? body.responseToken;
|
|
return token === undefined ? undefined : String(token);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
});
|
|
}
|
|
if (value instanceof Headers) {
|
|
return value.get("x-wrn-captcha-token") ?? value.get("x-captcha-token") ?? undefined;
|
|
}
|
|
if (value instanceof FormData || value instanceof URLSearchParams) {
|
|
const token = value.get(field) ?? value.get("captchaToken") ?? value.get("responseToken");
|
|
return typeof token === "string" ? token : undefined;
|
|
}
|
|
const token = value[field] ?? value.captchaToken ?? value.responseToken;
|
|
return token === undefined ? undefined : String(token);
|
|
}
|
|
|
|
export function captchaHeaders(token: string): HeadersInit {
|
|
return { "x-wrn-captcha-token": token };
|
|
}
|
|
|
|
export function captchaFields(
|
|
token: string,
|
|
field = "wrn-captcha-response",
|
|
): Record<string, string> {
|
|
return { [field]: token };
|
|
}
|
|
|
|
export async function verifyCaptcha(
|
|
providerOrEngine: CaptchaProvider | CaptchaEngine,
|
|
input: VerifyCaptchaInput,
|
|
): Promise<CaptchaVerificationResult> {
|
|
const provider: CaptchaProvider =
|
|
"client" in providerOrEngine
|
|
? (providerOrEngine as CaptchaProvider)
|
|
: selfHostedProvider(providerOrEngine as CaptchaEngine);
|
|
return provider.verify(input);
|
|
}
|
|
|
|
export async function verifyCaptchaOrThrow(
|
|
providerOrEngine: CaptchaProvider | CaptchaEngine,
|
|
input: VerifyCaptchaInput,
|
|
): Promise<CaptchaVerificationResult> {
|
|
const result = await verifyCaptcha(providerOrEngine, input);
|
|
if (!result.success) {
|
|
throw new Error(
|
|
`WRN-CAPTCHA-${String(result.code ?? "FAILED").toUpperCase()}: ${result.message ?? "CAPTCHA verification failed"}`,
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function captchaResultResponse(result: CaptchaVerificationResult): Response {
|
|
return Response.json(result, {
|
|
status: result.success ? 200 : 403,
|
|
headers: { "cache-control": "no-store", "x-content-type-options": "nosniff" },
|
|
});
|
|
}
|
|
|
|
export function captchaContext(ctx: Context): CaptchaVerificationResult | null {
|
|
const result = ctx.locals.captcha;
|
|
return result && typeof result === "object" ? (result as CaptchaVerificationResult) : null;
|
|
}
|