Functional Weave
Code in TypeScript

auth.login-throttle@1.0.0

impl/typescript.ts

1,964 bytes · the TypeScript implementation · view raw

import { type ThrottleDecision, type ThrottlePolicy } from "./auth_login_throttle_types.ts";

function isWhole(value: unknown): value is number {
  return typeof value === "number" && Number.isInteger(value);
}

/**
 * Allowed or locked, by replaying the failures in time order: maxAttempts
 * failures within windowSeconds of the latest lock the account for
 * lockoutSeconds from that failure, and the count starts again.
 */
export function loginThrottle(failures: readonly number[], now: number, policy: ThrottlePolicy): ThrottleDecision {
  if (!isWhole(policy.maxAttempts) || policy.maxAttempts < 1) throw new RangeError("maxAttempts must be a whole number of at least 1");
  if (!isWhole(policy.windowSeconds) || policy.windowSeconds < 1) throw new RangeError("windowSeconds must be a whole number of at least 1");
  if (!isWhole(policy.lockoutSeconds) || policy.lockoutSeconds < 1) throw new RangeError("lockoutSeconds must be a whole number of at least 1");
  if (!isWhole(now)) throw new TypeError("now must be a whole number of Unix seconds");
  if (!Array.isArray(failures) || !failures.every(isWhole)) throw new TypeError("failures must be whole Unix seconds");

  const times = failures.filter((t) => t <= now).sort((a, b) => a - b);
  let lockedUntil: number | null = null;
  let streak: number[] = [];
  for (const t of times) {
    if (lockedUntil !== null && t < lockedUntil) continue;
    streak = streak.filter((s) => s > t - policy.windowSeconds);
    streak.push(t);
    if (streak.length >= policy.maxAttempts) {
      lockedUntil = t + policy.lockoutSeconds;
      streak = [];
    }
  }
  if (lockedUntil !== null && now < lockedUntil) {
    return { allowed: false, retryAfterSeconds: lockedUntil - now, remainingAttempts: 0, lockedUntil };
  }
  const live = streak.filter((s) => s > now - policy.windowSeconds).length;
  return { allowed: true, retryAfterSeconds: 0, remainingAttempts: policy.maxAttempts - live, lockedUntil: null };
}