import type { AuthRiskDecision, AuthRiskSignals, AuthRiskLevel } from "./types.ts"; export interface RiskPolicy { captchaThreshold: number; mfaThreshold: number; blockThreshold: number; } function finiteScore(value: unknown, fallback = 0): number { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function threshold(value: number, fallback: number): number { return Math.min(100, Math.max(0, finiteScore(value, fallback))); } function level(score: number): AuthRiskLevel { if (score >= 90) return "critical"; if (score >= 65) return "high"; if (score >= 35) return "medium"; return "low"; } export function evaluateAuthRisk( signals: AuthRiskSignals = {}, policy: RiskPolicy = { captchaThreshold: 35, mfaThreshold: 60, blockThreshold: 90 }, ): AuthRiskDecision { let score = Math.max(0, finiteScore(signals.customScore)); const reasons: string[] = []; const add = (condition: boolean | undefined, points: number, reason: string) => { if (!condition) return; score += points; reasons.push(reason); }; const failedAttempts = Math.max(0, Math.floor(finiteScore(signals.failedAttempts))); if (failedAttempts > 0) { score += Math.min(45, failedAttempts * 10); reasons.push("failed-attempts"); } add(signals.unfamiliarDevice, 18, "unfamiliar-device"); add(signals.unusualIp, 20, "unusual-ip"); add(signals.impossibleTravel, 35, "impossible-travel"); add(signals.breachedPassword, 45, "breached-password"); add(signals.automationSuspected, 40, "automation-suspected"); add(signals.accountLocked, 100, "account-locked"); score = Math.min(100, Math.max(0, score)); const captchaThreshold = threshold(policy.captchaThreshold, 35); const mfaThreshold = threshold(policy.mfaThreshold, 60); const blockThreshold = threshold(policy.blockThreshold, 90); return { score, level: level(score), requireCaptcha: score >= captchaThreshold, requireMfa: score >= mfaThreshold, block: score >= blockThreshold, reasons, }; }