32 lines
977 B
TypeScript
32 lines
977 B
TypeScript
// Per-(slug, IP) token bucket. 30 requests in a 10s window. Cheap, in-memory,
|
|
// resets on process restart. Behind a load balancer each replica enforces its
|
|
// own bucket - that's fine, the goal is "stop a hostile script from melting one
|
|
// box", not perfect distributed accounting.
|
|
|
|
interface Bucket { tokens: number; refillAt: number }
|
|
|
|
const buckets = new Map<string, Bucket>();
|
|
const CAPACITY = 30;
|
|
const WINDOW_MS = 10_000;
|
|
|
|
export function allow(slug: string, ip: string): boolean {
|
|
const key = `${slug}\x00${ip}`;
|
|
const now = Date.now();
|
|
let b = buckets.get(key);
|
|
if (!b || now > b.refillAt) {
|
|
b = { tokens: CAPACITY, refillAt: now + WINDOW_MS };
|
|
buckets.set(key, b);
|
|
}
|
|
if (b.tokens <= 0) return false;
|
|
b.tokens--;
|
|
return true;
|
|
}
|
|
|
|
// Periodic sweep so the map doesn't grow forever.
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [k, b] of buckets) {
|
|
if (now > b.refillAt + WINDOW_MS) buckets.delete(k);
|
|
}
|
|
}, 60_000).unref?.();
|