interface RateLimitEntry { count: number; lastAttempt: number; locked: boolean; lockedUntil?: number; } const attempts = new Map(); const CLEANUP_INTERVAL = 10 * 60 * 1000; const RESET_WINDOW = 15 * 60 * 1000; const MAX_ATTEMPTS = 5; const LOCK_THRESHOLD = 10; const LOCK_DURATION = 15 * 60 * 1000; setInterval(() => { const now = Date.now(); for (const [key, entry] of attempts.entries()) { if (now - entry.lastAttempt > RESET_WINDOW) { attempts.delete(key); } } }, CLEANUP_INTERVAL); export function checkRateLimit(identifier: string) { const now = Date.now(); let entry = attempts.get(identifier); if (!entry) { attempts.set(identifier, { count: 1, lastAttempt: now, locked: false }); return { allowed: true, delayMs: 0, locked: false }; } if (now - entry.lastAttempt > RESET_WINDOW) { attempts.set(identifier, { count: 1, lastAttempt: now, locked: false }); return { allowed: true, delayMs: 0, locked: false }; } if (entry.locked && entry.lockedUntil) { if (now < entry.lockedUntil) { return { allowed: false, delayMs: entry.lockedUntil - now, locked: true }; } else { entry.locked = false; entry.count = 1; entry.lastAttempt = now; return { allowed: true, delayMs: 0, locked: false }; } } entry.count++; entry.lastAttempt = now; const delayMs = Math.min(Math.pow(2, entry.count - 2) * 1000, 30000); if (entry.count >= LOCK_THRESHOLD) { entry.locked = true; entry.lockedUntil = now + LOCK_DURATION; return { allowed: false, delayMs: LOCK_DURATION, locked: true }; } const allowed = entry.count <= MAX_ATTEMPTS; return { allowed, delayMs: allowed ? 0 : delayMs, locked: false }; } export function resetRateLimit(identifier: string) { attempts.delete(identifier); } export function getAttemptCount(identifier: string): number { return attempts.get(identifier)?.count ?? 0; }