const { sendError } = require("../utils/http"); const buckets = new Map(); function pruneExpired(now) { for (const [key, value] of buckets.entries()) { if (value.resetAt <= now) { buckets.delete(key); } } } function getClientIp(req) { const forwardedFor = req.headers["x-forwarded-for"]; if (typeof forwardedFor === "string" && forwardedFor.trim()) { return forwardedFor.split(",")[0].trim(); } return req.ip || req.socket?.remoteAddress || "unknown"; } function createRateLimit({ keyPrefix, windowMs, max, message, keyFn }) { return (req, res, next) => { const now = Date.now(); if (buckets.size > 5000) { pruneExpired(now); } const suffix = typeof keyFn === "function" ? keyFn(req) : getClientIp(req); const key = `${keyPrefix}:${suffix || "unknown"}`; const existing = buckets.get(key); const bucket = !existing || existing.resetAt <= now ? { count: 0, resetAt: now + windowMs } : existing; bucket.count += 1; buckets.set(key, bucket); if (bucket.count > max) { const retryAfterSeconds = Math.max( 1, Math.ceil((bucket.resetAt - now) / 1000) ); res.setHeader("Retry-After", String(retryAfterSeconds)); return sendError( res, 429, message || "Too many requests. Please try again later." ); } return next(); }; } module.exports = { createRateLimit, };