import type { Request, Response, NextFunction } from 'express'; // Per-IP fixed-window rate limiter for the public /v1 proxy (#35, item #6). // // The /v1 surface authenticates with the unified API key but has no password // login like the dashboard does, so without this an attacker who can reach the // server could brute-force the key or flood upstream providers. This caps how // many requests a single client IP can make per minute and returns a standard // OpenAI-shaped 429 once the cap is exceeded. // // FreeLLMAPI is a single-user tool, so the default ceiling is generous. Tune it // with PROXY_RATE_LIMIT_RPM (requests per minute per IP); set it to 0 to turn // rate limiting off entirely. const WINDOW_MS = 60_000; const DEFAULT_RPM = 120; // Bound the IP map so a flood of distinct (e.g. spoofed) source addresses can't // grow it without limit; expired entries are pruned opportunistically. const MAX_TRACKED_IPS = 10_000; interface WindowState { count: number; resetAt: number; } function parseLimit(): number { const raw = process.env.PROXY_RATE_LIMIT_RPM; if (raw === undefined || raw.trim() === '') return DEFAULT_RPM; const n = Number(raw); if (!Number.isFinite(n) || n < 0) return DEFAULT_RPM; return Math.floor(n); } export function createProxyRateLimiter() { const limit = parseLimit(); const windows = new Map(); return function proxyRateLimit(req: Request, res: Response, next: NextFunction): void { if (limit === 0) { next(); return; } const now = Date.now(); const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown'; let state = windows.get(ip); if (!state || now >= state.resetAt) { state = { count: 0, resetAt: now + WINDOW_MS }; windows.set(ip, state); } state.count += 1; if (windows.size > MAX_TRACKED_IPS) { for (const [key, value] of windows) { if (now >= value.resetAt) windows.delete(key); } } res.setHeader('X-RateLimit-Limit', String(limit)); res.setHeader('X-RateLimit-Remaining', String(Math.max(0, limit - state.count))); res.setHeader('X-RateLimit-Reset', String(Math.ceil(state.resetAt / 1000))); if (state.count > limit) { const retryAfter = Math.max(1, Math.ceil((state.resetAt - now) / 1000)); res.setHeader('Retry-After', String(retryAfter)); res.status(429).json({ error: { message: `Rate limit exceeded: more than ${limit} requests per minute. Retry in ${retryAfter}s.`, type: 'rate_limit_error', }, }); return; } next(); }; }