import { Context, Next } from 'hono'; import Redis from 'ioredis'; import { config } from '../config'; import { logger } from '../utils/logger'; import { getWorkerConfig } from '@wcag-ada/config'; const workerConfig = getWorkerConfig(); const redis = new Redis({ host: workerConfig.redis.host, port: workerConfig.redis.port, password: workerConfig.redis.password, db: workerConfig.redis.db, }); interface RateLimitOptions { limit?: number; window?: number; // in milliseconds keyGenerator?: (c: Context) => string; } export const rateLimiter = (options: RateLimitOptions = {}) => { const { limit = config.API_RATE_LIMIT, window = config.API_RATE_WINDOW, keyGenerator = (c) => { const userId = c.get('userId'); const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'; return userId ? `rate:user:${userId}` : `rate:ip:${ip}`; }, } = options; return async (c: Context, next: Next) => { const key = keyGenerator(c); try { // Get current count const current = await redis.get(key); const count = current ? parseInt(current) : 0; // Check if limit exceeded if (count >= limit) { const ttl = await redis.pttl(key); c.header('X-RateLimit-Limit', limit.toString()); c.header('X-RateLimit-Remaining', '0'); c.header('X-RateLimit-Reset', new Date(Date.now() + ttl).toISOString()); return c.json( { error: 'Rate limit exceeded', retryAfter: Math.ceil(ttl / 1000), }, 429 ); } // Increment counter const pipeline = redis.pipeline(); pipeline.incr(key); pipeline.pexpire(key, window); await pipeline.exec(); // Set headers c.header('X-RateLimit-Limit', limit.toString()); c.header('X-RateLimit-Remaining', (limit - count - 1).toString()); c.header('X-RateLimit-Reset', new Date(Date.now() + window).toISOString()); return next(); } catch (error) { logger.error('Rate limiter error:', error); // Continue on error - don't block requests due to rate limiter failure return next(); } }; };