/** * @cc-platform/server/rate — In-memory rate limiter middleware * * Extracted from Inventory-Manager-CC/web/server/server.js which has rate * limiting while remoteturtle does not. Both services benefit from this * protection. * * Usage: * const { createRateLimiter } = require('@cc-platform/server/rate'); * const limiter = createRateLimiter({ windowMs: 60000, maxRequests: 30 }); * app.use(limiter); * * // Or protect specific routes: * app.post('/api/sensitive', limiter, handler); */ 'use strict'; /** * Create an in-memory rate limiter middleware. * * @param {Object} [opts={}] - Options * @param {number} [opts.windowMs=60000] - Time window in milliseconds * @param {number} [opts.maxRequests=30] - Maximum requests per window per IP * @param {string} [opts.message] - Custom error message * @param {boolean} [opts.mutationsOnly=true] - Only rate-limit non-GET methods * @returns {Function} Express middleware */ function createRateLimiter(opts = {}) { const windowMs = opts.windowMs || 60000; const maxRequests = opts.maxRequests || 30; const message = opts.message || 'Too many requests — please try again later'; const mutationsOnly = opts.mutationsOnly !== false; // Map of IP → { count, resetTime } const clients = new Map(); // Periodic cleanup of expired entries const cleanupInterval = setInterval(() => { const now = Date.now(); for (const [ip, entry] of clients) { if (now > entry.resetTime) { clients.delete(ip); } } }, windowMs); // Don't prevent process exit if (cleanupInterval.unref) { cleanupInterval.unref(); } return function rateLimiter(req, res, next) { // Skip GET/HEAD/OPTIONS if mutationsOnly if (mutationsOnly && ['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); } const ip = req.ip || req.socket.remoteAddress || 'unknown'; const now = Date.now(); let entry = clients.get(ip); if (!entry || now > entry.resetTime) { entry = { count: 0, resetTime: now + windowMs }; clients.set(ip, entry); } entry.count++; if (entry.count > maxRequests) { const retryAfter = Math.ceil((entry.resetTime - now) / 1000); res.set('Retry-After', String(retryAfter)); return res.status(429).json({ error: message, retryAfter, }); } // Add rate limit headers res.set('X-RateLimit-Limit', String(maxRequests)); res.set('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count))); return next(); }; } module.exports = { createRateLimiter };