Files

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) })
};
}