/** * Per-IP sliding window, in memory. * * Deliberately simple: no Redis, no dependency. The trade-off is that the * counter lives in one process — with several instances behind a load balancer * each keeps its own, so the effective limit is per-instance, not global. For a * contact form that's fine (it raises the cost of flooding without pretending * to be airtight); a high-security form should put a real limiter in front. * * State is module-level, so it survives between requests but resets on redeploy * — acceptable for abuse throttling. */ const buckets = new Map(); /** Sweep expired buckets occasionally so the map doesn't grow unbounded. */ let lastSweep = Date.now(); const SWEEP_INTERVAL = 60_000; function sweep(now) { if (now - lastSweep < SWEEP_INTERVAL) { return; } lastSweep = now; for (const [key, bucket] of buckets){ if (bucket.resetAt <= now) { buckets.delete(key); } } } /** * Returns true when the request is within the limit, false when it should be * rejected. A missing key (no IP) is allowed through — better to accept a * submission than to block everyone behind a proxy that strips the header. */ export function checkRateLimit({ key, max = 5, windowMs = 60_000 }) { if (!key) { return true; } const now = Date.now(); sweep(now); const bucket = buckets.get(key); if (!bucket || bucket.resetAt <= now) { buckets.set(key, { count: 1, resetAt: now + windowMs }); return true; } if (bucket.count >= max) { return false; } bucket.count += 1; return true; } //# sourceMappingURL=rateLimit.js.map //# sourceMappingURL=rateLimit.js.map