feat: add fixed-window rate limiter for abuse protection
This commit is contained in:
57
src/lib/rate-limit.js
Normal file
57
src/lib/rate-limit.js
Normal file
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
class FixedWindowRateLimiter {
|
||||
constructor({ limit, windowMs }) {
|
||||
if (!Number.isInteger(limit) || limit <= 0) {
|
||||
throw new Error("limit_must_be_positive_integer");
|
||||
}
|
||||
if (!Number.isInteger(windowMs) || windowMs <= 0) {
|
||||
throw new Error("window_ms_must_be_positive_integer");
|
||||
}
|
||||
|
||||
this.limit = limit;
|
||||
this.windowMs = windowMs;
|
||||
this.store = new Map();
|
||||
}
|
||||
|
||||
hit(key, nowMs) {
|
||||
const now = Number.isInteger(nowMs) ? nowMs : Date.now();
|
||||
const safeKey = key || "anonymous";
|
||||
|
||||
const current = this.store.get(safeKey);
|
||||
if (!current || current.windowEnd <= now) {
|
||||
const next = {
|
||||
count: 1,
|
||||
windowEnd: now + this.windowMs,
|
||||
};
|
||||
this.store.set(safeKey, next);
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: this.limit - 1,
|
||||
retryAfterSec: 0,
|
||||
};
|
||||
}
|
||||
|
||||
current.count += 1;
|
||||
const allowed = current.count <= this.limit;
|
||||
|
||||
return {
|
||||
allowed,
|
||||
remaining: Math.max(0, this.limit - current.count),
|
||||
retryAfterSec: allowed ? 0 : Math.ceil((current.windowEnd - now) / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
cleanup(nowMs) {
|
||||
const now = Number.isInteger(nowMs) ? nowMs : Date.now();
|
||||
for (const [key, value] of this.store.entries()) {
|
||||
if (value.windowEnd <= now) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FixedWindowRateLimiter,
|
||||
};
|
||||
Reference in New Issue
Block a user