/** * Login and signup throttling, keyed by email and by client address. * * In-memory on purpose: it protects a single process against online guessing * and resets on restart. Anything stronger belongs at the reverse proxy. */ export class LoginThrottle { private readonly hits = new Map(); constructor( private readonly limit = 8, private readonly windowMs = 5 * 60_000, private readonly lockoutMs = 15 * 60_000, ) {} /** Milliseconds the caller must wait, or 0 when the attempt may proceed. */ check(key: string): number { const entry = this.hits.get(key); if (!entry) return 0; const now = Date.now(); if (entry.until > now) return entry.until - now; if (now - entry.first > this.windowMs) { this.hits.delete(key); return 0; } return 0; } fail(key: string): void { const now = Date.now(); const entry = this.hits.get(key); if (!entry || now - entry.first > this.windowMs) { this.hits.set(key, { count: 1, first: now, until: 0 }); return; } entry.count++; if (entry.count >= this.limit) { entry.until = now + this.lockoutMs; entry.count = 0; entry.first = now; } } succeed(key: string): void { this.hits.delete(key); } clear(): void { this.hits.clear(); } }