Session 7d: Audit fixes - rate limiting, error leak, parallel parlays, analyze cache, bundle analyzer

This commit is contained in:
Kev
2026-06-10 03:12:20 -04:00
parent d954e4d952
commit 6f4a353de9
18 changed files with 913 additions and 72 deletions
+76
View File
@@ -0,0 +1,76 @@
/**
* IP-keyed rate-limit middleware.
*
* Sliding-window counter per remote IP, kept in-memory. Use for routes
* that take expensive upstream calls and aren't behind requireAuth —
* the public demo (/api/analyze/*) is the canonical caller. Auth'd
* routes are gated by tier, which already throttles abuse.
*
* Why in-memory not Redis: this middleware is the FIRST line of defense
* — it must not depend on Redis being warm. If Redis is down the API
* still serves and this still throttles. Memory cost is bounded by
* MAX_TRACKED_IPS (the LRU-style trim on overflow).
*
* Why not the factory in src/utils/rateLimiter.js: that gives one bucket
* per call site. We need one bucket PER IP.
*/
const DEFAULT_WINDOW_MS = 60_000;
const DEFAULT_MAX = 10;
const MAX_TRACKED_IPS = 10_000;
function clientIp(req) {
// Express's req.ip respects trust proxy; fall back to socket if not.
return req.ip || req.socket?.remoteAddress || 'unknown';
}
function createRateLimit({ windowMs = DEFAULT_WINDOW_MS, max = DEFAULT_MAX, key = clientIp } = {}) {
// Map preserves insertion order; oldest-IP eviction is O(1) via shift.
const hits = new Map();
function evictIfFull() {
if (hits.size <= MAX_TRACKED_IPS) return;
// Drop the oldest entry — Map.keys() yields in insertion order.
const first = hits.keys().next().value;
if (first !== undefined) hits.delete(first);
}
function pruneOlderThan(timestamps, cutoff) {
// In-place filter (mutating the array end-to-start), faster than
// building a new array on every request. Returns the surviving count.
let writeIdx = 0;
for (let i = 0; i < timestamps.length; i += 1) {
if (timestamps[i] > cutoff) {
timestamps[writeIdx] = timestamps[i];
writeIdx += 1;
}
}
timestamps.length = writeIdx;
return writeIdx;
}
return function rateLimit(req, res, next) {
const id = key(req);
const now = Date.now();
const cutoff = now - windowMs;
let timestamps = hits.get(id);
if (!timestamps) {
timestamps = [];
hits.set(id, timestamps);
evictIfFull();
}
const remaining = pruneOlderThan(timestamps, cutoff);
if (remaining >= max) {
const retryAfterSec = Math.max(1, Math.ceil((timestamps[0] + windowMs - now) / 1000));
res.set('Retry-After', String(retryAfterSec));
return res.status(429).json({ error: 'Too many requests' });
}
timestamps.push(now);
return next();
};
}
module.exports = { createRateLimit, __internals: { clientIp, MAX_TRACKED_IPS } };