| |
| |
| |
| |
| |
| import type { MiddlewareHandler } from 'hono'; |
| import type { Env } from '../types/env'; |
|
|
| const RATE_LIMIT_WINDOW_MS = 60_000; |
| const RATE_LIMIT_MAX_REQUESTS = 20; |
|
|
| 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 { |
| |
| 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(); |
| }; |
|
|