51 lines
1.7 KiB
JavaScript
51 lines
1.7 KiB
JavaScript
/**
|
|
* Per-IP sliding window, in memory.
|
|
*
|
|
* Deliberately simple: no Redis, no dependency. The trade-off is that the
|
|
* counter lives in one process — with several instances behind a load balancer
|
|
* each keeps its own, so the effective limit is per-instance, not global. For a
|
|
* contact form that's fine (it raises the cost of flooding without pretending
|
|
* to be airtight); a high-security form should put a real limiter in front.
|
|
*
|
|
* State is module-level, so it survives between requests but resets on redeploy
|
|
* — acceptable for abuse throttling.
|
|
*/ const buckets = new Map();
|
|
/** Sweep expired buckets occasionally so the map doesn't grow unbounded. */ let lastSweep = Date.now();
|
|
const SWEEP_INTERVAL = 60_000;
|
|
function sweep(now) {
|
|
if (now - lastSweep < SWEEP_INTERVAL) {
|
|
return;
|
|
}
|
|
lastSweep = now;
|
|
for (const [key, bucket] of buckets){
|
|
if (bucket.resetAt <= now) {
|
|
buckets.delete(key);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Returns true when the request is within the limit, false when it should be
|
|
* rejected. A missing key (no IP) is allowed through — better to accept a
|
|
* submission than to block everyone behind a proxy that strips the header.
|
|
*/ export function checkRateLimit({ key, max = 5, windowMs = 60_000 }) {
|
|
if (!key) {
|
|
return true;
|
|
}
|
|
const now = Date.now();
|
|
sweep(now);
|
|
const bucket = buckets.get(key);
|
|
if (!bucket || bucket.resetAt <= now) {
|
|
buckets.set(key, {
|
|
count: 1,
|
|
resetAt: now + windowMs
|
|
});
|
|
return true;
|
|
}
|
|
if (bucket.count >= max) {
|
|
return false;
|
|
}
|
|
bucket.count += 1;
|
|
return true;
|
|
}
|
|
|
|
//# sourceMappingURL=rateLimit.js.map
|