File
Blob: src/worker/middleware/rate-limit.ts
| 1 | import { createMiddleware } from "hono/factory"; |
| 2 | import type { users } from "@/worker/db/d1/schema"; |
| 3 | import { createLogger } from "@/worker/lib/logger"; |
| 4 | import { CF_IP_HEADER } from "@/worker/lib/constants"; |
| 5 | import { isLocalRequestUrl } from "@/worker/http"; |
| 6 | |
| 7 | const log = createLogger("rate-limit"); |
| 8 | |
| 9 | export function rateLimit(binding: "RL_AUTH" | "RL_API" | "RL_AI") { |
| 10 | return createMiddleware<{ |
| 11 | Bindings: Env; |
| 12 | Variables: { |
| 13 | user: typeof users.$inferSelect | null; |
| 14 | }; |
| 15 | }>(async (c, next) => { |
| 16 | // Local dev and browser-driven E2E run through a single loopback IP and can |
| 17 | // legitimately make bursts of auth/refresh calls during page bootstrap. |
| 18 | // Keep production rate limits intact while avoiding flaky local lockouts. |
| 19 | if (isLocalRequestUrl(c.req.url)) { |
| 20 | await next(); |
| 21 | return; |
| 22 | } |
| 23 | |
| 24 | const key = |
| 25 | binding === "RL_AUTH" |
| 26 | ? (c.req.header(CF_IP_HEADER) ?? "unknown") |
| 27 | : (c.get("user")?.id ?? c.req.header(CF_IP_HEADER) ?? "unknown"); |
| 28 | |
| 29 | const { success } = await c.env[binding].limit({ key }); |
| 30 | if (!success) { |
| 31 | log.info("rate_limit_exceeded", { binding, key, path: c.req.path }); |
| 32 | return c.json({ error: "rate_limited", message: "Too many requests" }, 429); |
| 33 | } |
| 34 | |
| 35 | await next(); |
| 36 | }); |
| 37 | } |