/** * Rate Limit Middleware * Uses KV store for sliding window rate limiting * Lightweight - primarily I/O bound (KV read/write), minimal CPU */ import type { MiddlewareHandler } from 'hono'; import type { Env } from '../types/env'; const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute const RATE_LIMIT_MAX_REQUESTS = 20; // 20 requests per minute per IP export const rateLimitMiddleware: MiddlewareHandler<{ Bindings: Env }> = async (c, next) => { const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'; const key = `ratelimit:${ip}`; const raw = await c.env.WFO_CACHE.get(key, 'text'); const now = Date.now(); let count = 1; if (raw) { const entry = JSON.parse(raw) as { count: number; windowStart: number }; if (now - entry.windowStart < RATE_LIMIT_WINDOW_MS) { count = entry.count + 1; if (count > RATE_LIMIT_MAX_REQUESTS) { return c.json({ success: false, error: 'Rate limit exceeded. Max 20 requests/minute.', retryAfter: Math.ceil((entry.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000), }, 429); } await c.env.WFO_CACHE.put(key, JSON.stringify({ count, windowStart: entry.windowStart }), { expirationTtl: 120, }); } else { // New window await c.env.WFO_CACHE.put(key, JSON.stringify({ count: 1, windowStart: now }), { expirationTtl: 120, }); } } else { await c.env.WFO_CACHE.put(key, JSON.stringify({ count: 1, windowStart: now }), { expirationTtl: 120, }); } c.res.headers.set('X-RateLimit-Limit', String(RATE_LIMIT_MAX_REQUESTS)); c.res.headers.set('X-RateLimit-Remaining', String(Math.max(0, RATE_LIMIT_MAX_REQUESTS - count))); await next(); };