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