export type Bucket = { count: number; resetAt: number }; export type RateLimitStore = Pick< Map, "get" | "set" | "delete" | "size" > & { [Symbol.iterator](): IterableIterator<[string, Bucket]>; }; export type RateLimitDeps = { now?: () => number; store?: RateLimitStore; ipOf?: (request: Request) => string; }; const defaultStore: RateLimitStore = new Map(); function defaultIpOf(request: Request): string { const fwd = request.headers.get("x-forwarded-for"); if (fwd) return fwd.split(",")[0].trim(); const real = request.headers.get("x-real-ip"); if (real) return real.trim(); return "unknown"; } export function checkRateLimit( request: Request, opts: { key: string; limit: number; windowMs: number }, deps: RateLimitDeps = {} ): Response | null { const now = (deps.now ?? Date.now)(); const store = deps.store ?? defaultStore; const ip = (deps.ipOf ?? defaultIpOf)(request); const bucketKey = `${opts.key}:${ip}`; const bucket = store.get(bucketKey); if (!bucket || now >= bucket.resetAt) { store.set(bucketKey, { count: 1, resetAt: now + opts.windowMs }); if (store.size > 10000) { for (const [k, b] of store) { if (b.resetAt <= now) store.delete(k); } } return null; } if (bucket.count >= opts.limit) { const retryAfter = Math.ceil((bucket.resetAt - now) / 1000); return Response.json( { error: "Too many requests" }, { status: 429, headers: { "Retry-After": String(retryAfter) }, } ); } bucket.count += 1; return null; }