pingql/apps/status/src/rate-limit.ts

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?.();