49 lines
1.6 KiB
JavaScript
49 lines
1.6 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
|
|
const WINDOW_MS = 60 * 1000;
|
|
const buckets = new Map();
|
|
|
|
function prune(now) {
|
|
if (buckets.size < 2048) return;
|
|
for (const [key, bucket] of buckets) {
|
|
if (bucket.resetAt <= now) buckets.delete(key);
|
|
}
|
|
}
|
|
|
|
export function rateLimitIdentity({ token = "", ipAddress = "" } = {}) {
|
|
const normalizedToken = String(token || "").trim();
|
|
if (normalizedToken) return `token:${createHash("sha256").update(normalizedToken).digest("hex")}`;
|
|
return `ip:${String(ipAddress || "unknown").trim() || "unknown"}`;
|
|
}
|
|
|
|
export function consumeRateLimit({ key, limit, now = Date.now() } = {}) {
|
|
const normalizedLimit = Math.max(0, Math.floor(Number(limit || 0)));
|
|
if (!normalizedLimit) return null;
|
|
const windowStart = Math.floor(now / WINDOW_MS) * WINDOW_MS;
|
|
const bucketKey = `${String(key || "unknown")}:${windowStart}`;
|
|
const resetAt = windowStart + WINDOW_MS;
|
|
const current = buckets.get(bucketKey) || { count: 0, resetAt };
|
|
current.count += 1;
|
|
buckets.set(bucketKey, current);
|
|
prune(now);
|
|
return {
|
|
allowed: current.count <= normalizedLimit,
|
|
count: current.count,
|
|
limit: normalizedLimit,
|
|
remaining: Math.max(0, normalizedLimit - current.count),
|
|
resetAt,
|
|
retryAfter: Math.max(1, Math.ceil((resetAt - now) / 1000))
|
|
};
|
|
}
|
|
|
|
export function rateLimitHeaders(result) {
|
|
if (!result) return {};
|
|
return {
|
|
"x-ratelimit-limit": String(result.limit),
|
|
"x-ratelimit-remaining": String(result.remaining),
|
|
"x-ratelimit-reset": String(Math.ceil(result.resetAt / 1000)),
|
|
...(result.allowed ? {} : { "retry-after": String(result.retryAfter) })
|
|
};
|
|
}
|
|
|