feat: add fixed-window rate limiter for abuse protection

This commit is contained in:
Codex
2026-02-18 13:01:19 +00:00
parent 6bd238ad0e
commit a9ef1e5e23
2 changed files with 94 additions and 0 deletions

57
src/lib/rate-limit.js Normal file
View 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,
};