Files
cc-platform-core/server/rate.js
2026-03-26 15:00:49 -04:00

88 lines
2.5 KiB
JavaScript

/**
* @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 };