/** * In-memory sliding-window rate limiter. * * Suitable for single-node deployments (pilot scale). * For multi-replica, replace with Redis-backed store. */ interface RateLimitEntry { timestamps: number[]; } const store = new Map(); // Cleanup stale entries every 10 minutes if (typeof globalThis !== "undefined") { // Use globalThis to survive HMR in dev — only one interval const key = "__rateLimitCleanup"; if (!(globalThis as any)[key]) { (globalThis as any)[key] = setInterval(() => { const now = Date.now(); for (const [k, entry] of store) { entry.timestamps = entry.timestamps.filter((t) => now - t < 3_600_000); if (entry.timestamps.length === 0) store.delete(k); } }, 600_000); } } export interface RateLimitResult { allowed: boolean; remaining: number; /** Milliseconds until the oldest request in the window expires */ resetMs: number; } /** * Check and record a rate-limited action. * * @param key - Unique key, e.g. `register:${ip}` * @param limit - Max allowed actions in the window * @param windowMs - Window size in milliseconds */ export function rateLimit( key: string, limit: number, windowMs: number, ): RateLimitResult { const now = Date.now(); const entry = store.get(key) ?? { timestamps: [] }; // Prune expired timestamps entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs); if (entry.timestamps.length >= limit) { const oldest = entry.timestamps[0]; return { allowed: false, remaining: 0, resetMs: oldest + windowMs - now, }; } entry.timestamps.push(now); store.set(key, entry); return { allowed: true, remaining: limit - entry.timestamps.length, resetMs: entry.timestamps[0] + windowMs - now, }; }