-
-
Notifications
You must be signed in to change notification settings - Fork 444
Expand file tree
/
Copy pathbadge-rate-limit.ts
More file actions
76 lines (62 loc) · 2.25 KB
/
Copy pathbadge-rate-limit.ts
File metadata and controls
76 lines (62 loc) · 2.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { NextRequest } from "next/server";
const WINDOW_MS = 60 * 1000;
const BADGE_LIMIT = 20;
// Sliding window counter entry — O(1) per client instead of O(N) timestamps.
type Entry = {
prevCount: number; // requests counted in the previous window
currCount: number; // requests counted in the current window
windowStart: number; // epoch ms, quantized to WINDOW_MS boundaries
};
const store = new Map<string, Entry>();
export type BadgeRateLimitResult = {
allowed: boolean;
remaining: number;
reset: number;
};
let lastPrune = Date.now();
function pruneStore(now: number): void {
if (store.size < 500) return;
if (now - lastPrune < 60000) return; // Only prune once per minute
lastPrune = now;
const cutoff = now - WINDOW_MS;
for (const [key, entry] of store) {
if (entry.windowStart < cutoff) store.delete(key);
}
}
export function checkBadgeRateLimit(ip: string): BadgeRateLimitResult {
const now = Date.now();
pruneStore(now);
const key = `badge:${ip}`;
const windowStart = Math.floor(now / WINDOW_MS) * WINDOW_MS;
const reset = Math.ceil((windowStart + WINDOW_MS) / 1000);
let entry = store.get(key);
if (!entry || entry.windowStart < windowStart - WINDOW_MS) {
entry = { prevCount: 0, currCount: 0, windowStart };
} else if (entry.windowStart < windowStart) {
entry = { prevCount: entry.currCount, currCount: 0, windowStart };
}
const elapsed = now - windowStart;
const prevWeight = 1 - elapsed / WINDOW_MS;
const estimate = Math.floor(entry.prevCount * prevWeight) + entry.currCount;
if (estimate >= BADGE_LIMIT) {
store.set(key, entry);
return { allowed: false, remaining: 0, reset };
}
entry = { ...entry, currCount: entry.currCount + 1 };
store.set(key, entry);
const remaining = Math.max(
0,
BADGE_LIMIT - Math.floor(entry.prevCount * prevWeight) - entry.currCount
);
return { allowed: true, remaining, reset };
}
export function getBadgeClientIp(req: NextRequest): string {
// Use Vercel's trusted IP header or fallback to connection IP
// Do NOT blindly trust x-forwarded-for as it can be easily spoofed
return (
req.headers.get("x-vercel-forwarded-for") ||
req.headers.get("x-real-ip") ||
(req as NextRequest & { ip?: string }).ip ||
"unknown"
);
}