/** * In-memory rate limiter (per IP, per minute window). * For production at scale, replace with Redis-backed solution. */ interface RateLimitEntry { count: number; windowStart: number; } const store = new Map(); const WINDOW_MS = 60_000; // 1 minute // Cleanup stale entries every 5 minutes to prevent memory leaks setInterval(() => { const now = Date.now(); for (const [key, entry] of store.entries()) { if (now - entry.windowStart > WINDOW_MS * 2) { store.delete(key); } } }, 300_000); export function checkRateLimit( ip: string, limit: number = parseInt(process.env.RATE_LIMIT_RPM ?? "100"), ): { allowed: boolean; remaining: number; resetAt: number } { const now = Date.now(); const entry = store.get(ip); if (!entry || now - entry.windowStart > WINDOW_MS) { store.set(ip, { count: 1, windowStart: now }); return { allowed: true, remaining: limit - 1, resetAt: now + WINDOW_MS }; } if (entry.count >= limit) { return { allowed: false, remaining: 0, resetAt: entry.windowStart + WINDOW_MS, }; } entry.count++; return { allowed: true, remaining: limit - entry.count, resetAt: entry.windowStart + WINDOW_MS, }; } /** Extract real IP from request (handles proxies). */ export function getClientIp(request: Request): string { return ( request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown" ); }