/** Fixed-window in-memory limiter. Fine for a single server process; move to the database if we scale out. */ export class RateLimiter { private hits = new Map() private readonly limit: number private readonly windowMs: number constructor(limit: number, windowMs: number) { this.limit = limit this.windowMs = windowMs } /** Records an attempt; returns false once the key has exceeded the limit. */ attempt(key: string, now = Date.now()): boolean { const entry = this.hits.get(key) if (!entry || entry.resetAt <= now) { this.hits.set(key, { count: 1, resetAt: now + this.windowMs }) if (this.hits.size > 10_000) this.prune(now) return true } entry.count += 1 return entry.count <= this.limit } reset(key: string): void { this.hits.delete(key) } private prune(now: number) { for (const [key, entry] of this.hits) if (entry.resetAt <= now) this.hits.delete(key) } }