import type { CaptchaPolicyOptions, CaptchaRiskResult, CaptchaRiskSignals } from "./types.ts"; export function evaluateCaptchaRisk( signals: CaptchaRiskSignals, threshold = 50, ): CaptchaRiskResult { let score = Math.max(0, Math.min(100, signals.customScore ?? 0)); const reasons: string[] = []; const add = (points: number, reason: string): void => { score = Math.min(100, score + points); reasons.push(reason); }; if ((signals.failedAttempts ?? 0) > 0) add(Math.min(35, (signals.failedAttempts ?? 0) * 10), "failed-attempts"); if ((signals.requestsInWindow ?? 0) > 20) add(Math.min(35, ((signals.requestsInWindow ?? 0) - 20) * 2), "request-rate"); if (signals.completionMs !== undefined && signals.completionMs < 700) add(25, "too-fast"); if (signals.missingBrowserSignals) add(20, "missing-browser-signals"); if (signals.suspiciousHeaders) add(20, "suspicious-headers"); if (signals.tokenReuse) add(70, "token-reuse"); if (signals.knownBadIp) add(60, "known-bad-ip"); return { score, challenge: score >= threshold, reasons }; } export function shouldRequireCaptcha( action: string, options: CaptchaPolicyOptions = {}, signals: CaptchaRiskSignals = {}, ): CaptchaRiskResult { if (options.neverForActions?.includes(action) || options.mode === "never") { return { score: 0, challenge: false, reasons: ["policy-never"] }; } if (options.alwaysForActions?.includes(action) || options.mode === "always") { return { score: 100, challenge: true, reasons: ["policy-always"] }; } if ((options.mode ?? "adaptive") === "session") { return { score: 100, challenge: true, reasons: ["session-unverified"] }; } return evaluateCaptchaRisk(signals, options.threshold ?? 50); } export interface CaptchaSessionGrant { action: string; routeGroup?: string; expiresAt: number; provider: string; } export function validCaptchaGrant( grants: CaptchaSessionGrant[] | undefined, action: string, now: number, routeGroup?: string, ): CaptchaSessionGrant | undefined { return grants?.find( (grant) => grant.expiresAt > now && (grant.action === action || grant.action === "*") && (!routeGroup || !grant.routeGroup || grant.routeGroup === routeGroup), ); }