import { Context } from 'hono'; import { getRateLimiter } from './stores/rateLimiter.js'; /** * Rate limiting helpers. * * The actual counting is delegated to a pluggable rate limiter (in-memory by * default, Redis-backed when REDIS_URL is set) so limits are enforced either * per instance (single-instance deployments) or across all instances * (horizontal scaling). See lib/stores/rateLimiter.ts. */ /** Best-effort client IP extraction (honours common reverse-proxy headers). */ export function getClientIp(c: Context): string { const forwarded = c.req.header('x-forwarded-for'); if (forwarded) return forwarded.split(',')[0].trim(); return c.req.header('x-real-ip') || 'unknown'; } /** * Consume one unit against a key. Returns whether the request is allowed and, * when blocked, how many seconds until the window resets. */ export function consumeRateLimit( key: string, max: number, windowMs: number ): Promise<{ allowed: boolean; retryAfter?: number }> { return getRateLimiter().consume(key, max, windowMs); } /** * Hono middleware factory that rate-limits by client IP. * Use a distinct `prefix` per endpoint group so unrelated routes don't share a bucket. */ export function rateLimitMiddleware(opts: { max: number; windowMs: number; prefix: string }) { return async (c: Context, next: () => Promise) => { const ip = getClientIp(c); const result = await consumeRateLimit(`${opts.prefix}:${ip}`, opts.max, opts.windowMs); if (!result.allowed) { return c.json( { error: 'Too many requests. Please try again later.', retryAfter: result.retryAfter }, 429 ); } await next(); }; }