Spaces:
Runtime error
Runtime error
| // 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; | |
| function parseLimit() { | |
| 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, res, next) { | |
| 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(); | |
| }; | |
| } | |
| //# sourceMappingURL=rateLimit.js.map |