/** * Brute-force lockout as a pure reducer so POS and back office share the exact * policy and it is testable without a clock. Attempts are audit-logged by the * caller (I7: every mutation audit-logged in the same transaction). */ export interface LockoutPolicy { maxFailures: number lockMs: number } export const DEFAULT_LOCKOUT: LockoutPolicy = { maxFailures: 5, lockMs: 60_000 } export interface LockoutState { failures: number lockedUntilMs?: number } export const FRESH_LOCKOUT: LockoutState = { failures: 0 } export function isLocked(state: LockoutState, nowMs: number): boolean { return state.lockedUntilMs !== undefined && nowMs < state.lockedUntilMs } export function recordFailure( state: LockoutState, nowMs: number, policy: LockoutPolicy = DEFAULT_LOCKOUT, ): LockoutState { const failures = state.failures + 1 if (failures >= policy.maxFailures) { return { failures, lockedUntilMs: nowMs + policy.lockMs } } return { failures } } export function recordSuccess(): LockoutState { return FRESH_LOCKOUT }