Files
WRNexusJS/packages/auth/src/risk.ts
T
2026-07-29 12:51:10 +05:30

61 lines
2.0 KiB
TypeScript

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,
};
}