● Zion Boggan
repos/Docket/middleware.ts
zionboggan.com ↗
57 lines · typescript
History for this file →
1
import { NextResponse, type NextRequest } from "next/server";
2
 
3
 
4
const WINDOW_MS = 60_000;
5
const MAX_REQ = 120;
6
 
7
type Bucket = { count: number; resetAt: number };
8
const g = globalThis as unknown as { __docketBuckets?: Map<string, Bucket> };
9
const buckets: Map<string, Bucket> = (g.__docketBuckets ??= new Map());
10
 
11
function clientIp(req: NextRequest): string {
12
  const cf = req.headers.get("cf-connecting-ip");
13
  if (cf) return cf.trim();
14
  const real = req.headers.get("x-real-ip");
15
  if (real) return real.trim();
16
  const xff = req.headers.get("x-forwarded-for");
17
  if (xff) return xff.split(",")[0].trim();
18
  return "unknown";
19
}
20
 
21
export function middleware(req: NextRequest) {
22
  if (buckets.size > 10_000) {
23
    const now = Date.now();
24
    for (const [k, b] of buckets) if (b.resetAt < now) buckets.delete(k);
25
  }
26
 
27
  const ip = clientIp(req);
28
  const now = Date.now();
29
  const b = buckets.get(ip);
30
 
31
  if (!b || b.resetAt < now) {
32
    buckets.set(ip, { count: 1, resetAt: now + WINDOW_MS });
33
    return NextResponse.next();
34
  }
35
 
36
  b.count += 1;
37
  if (b.count > MAX_REQ) {
38
    const retry = Math.ceil((b.resetAt - now) / 1000);
39
    return new NextResponse(
40
      JSON.stringify({ error: "Rate limit exceeded. Slow down." }),
41
      {
42
        status: 429,
43
        headers: {
44
          "Content-Type": "application/json",
45
          "Retry-After": String(retry),
46
          "X-RateLimit-Limit": String(MAX_REQ),
47
          "X-RateLimit-Remaining": "0",
48
        },
49
      }
50
    );
51
  }
52
  return NextResponse.next();
53
}
54
 
55
export const config = {
56
  matcher: ["/api/:path*"],
57
};