| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import type { Request, Response, NextFunction } from 'express' |
| |
|
| | interface RateBucket { |
| | totalRequests: number |
| | minuteRequests: number[] |
| | dayRequests: number[] |
| | } |
| |
|
| | const TOTAL_LIMIT = parseInt(process.env.RATE_LIMIT_TOTAL || '5', 10) |
| | const MINUTE_LIMIT = parseInt(process.env.RATE_LIMIT_PER_MINUTE || '60', 10) |
| | const DAY_LIMIT = parseInt(process.env.RATE_LIMIT_PER_DAY || '1000', 10) |
| | const MINUTE_MS = 60 * 1000 |
| | const DAY_MS = 24 * 60 * 60 * 1000 |
| |
|
| | const buckets = new Map<string, RateBucket>() |
| |
|
| | |
| | |
| | setInterval(() => { |
| | const now = Date.now() |
| | for (const [key, bucket] of buckets) { |
| | bucket.minuteRequests = bucket.minuteRequests.filter(t => now - t < MINUTE_MS) |
| | bucket.dayRequests = bucket.dayRequests.filter(t => now - t < DAY_MS) |
| | |
| | } |
| | }, 10 * 60 * 1000) |
| |
|
| | export function rateLimit(req: Request, res: Response, next: NextFunction): void { |
| | const keyId = (req as any).apiKeyId || 'unknown' |
| | const now = Date.now() |
| |
|
| | if (!buckets.has(keyId)) { |
| | buckets.set(keyId, { totalRequests: 0, minuteRequests: [], dayRequests: [] }) |
| | } |
| |
|
| | const bucket = buckets.get(keyId)! |
| |
|
| | |
| | if (TOTAL_LIMIT > 0 && bucket.totalRequests >= TOTAL_LIMIT) { |
| | res.status(429).json({ |
| | error: 'Request limit reached for this API key', |
| | limit: TOTAL_LIMIT, |
| | used: bucket.totalRequests, |
| | remaining: 0, |
| | note: 'This is a research preview with a limited number of requests per key. Contact the API host for more access.', |
| | }) |
| | return |
| | } |
| |
|
| | |
| | bucket.minuteRequests = bucket.minuteRequests.filter(t => now - t < MINUTE_MS) |
| | bucket.dayRequests = bucket.dayRequests.filter(t => now - t < DAY_MS) |
| |
|
| | |
| | if (bucket.minuteRequests.length >= MINUTE_LIMIT) { |
| | const retryAfter = Math.ceil((bucket.minuteRequests[0] + MINUTE_MS - now) / 1000) |
| | res.status(429).json({ |
| | error: 'Rate limit exceeded (per-minute)', |
| | limit: MINUTE_LIMIT, |
| | window: '1 minute', |
| | retry_after_seconds: retryAfter, |
| | }) |
| | return |
| | } |
| |
|
| | |
| | if (bucket.dayRequests.length >= DAY_LIMIT) { |
| | const retryAfter = Math.ceil((bucket.dayRequests[0] + DAY_MS - now) / 1000) |
| | res.status(429).json({ |
| | error: 'Rate limit exceeded (daily)', |
| | limit: DAY_LIMIT, |
| | window: '24 hours', |
| | retry_after_seconds: retryAfter, |
| | }) |
| | return |
| | } |
| |
|
| | |
| | bucket.totalRequests++ |
| | bucket.minuteRequests.push(now) |
| | bucket.dayRequests.push(now) |
| |
|
| | |
| | const totalRemaining = TOTAL_LIMIT > 0 ? TOTAL_LIMIT - bucket.totalRequests : Infinity |
| | res.setHeader('X-RateLimit-Limit-Total', TOTAL_LIMIT > 0 ? TOTAL_LIMIT : 'unlimited') |
| | res.setHeader('X-RateLimit-Remaining-Total', TOTAL_LIMIT > 0 ? totalRemaining : 'unlimited') |
| | res.setHeader('X-RateLimit-Limit-Minute', MINUTE_LIMIT) |
| | res.setHeader('X-RateLimit-Remaining-Minute', MINUTE_LIMIT - bucket.minuteRequests.length) |
| | res.setHeader('X-RateLimit-Limit-Day', DAY_LIMIT) |
| | res.setHeader('X-RateLimit-Remaining-Day', DAY_LIMIT - bucket.dayRequests.length) |
| |
|
| | next() |
| | } |
| |
|