'use strict' // Rate-limiter EN MÉMOIRE (fenêtre glissante), par IP × bucket. Aucune dépendance, FAIL-OPEN (toute erreur = // autorise). But : freiner l'énumération de tokens / brute-force OTP sur les routes PUBLIQUES sensibles, sans // jamais bloquer un vrai usager (limites généreuses). Derrière Traefik → vraie IP dans X-Forwarded-For. const buckets = new Map() // "ip|bucket" → [timestamps ms] let _lastSweep = 0 function clientIp (req) { const xff = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim() return xff || (req.socket && req.socket.remoteAddress) || 'unknown' } // true = la requête DOIT être bloquée (limite atteinte dans la fenêtre). function limited (req, bucket, max, windowMs) { try { const key = clientIp(req) + '|' + bucket const now = Date.now(); const cut = now - windowMs let arr = buckets.get(key); if (!arr) { arr = []; buckets.set(key, arr) } while (arr.length && arr[0] < cut) arr.shift() if (arr.length >= max) return true arr.push(now) return false } catch (e) { return false } // fail-open : ne jamais casser une requête à cause du limiteur } // Purge périodique des buckets vidés/anciens (borne la mémoire) — appelée à l'occasion, pas de timer. function maybeSweep (windowMs) { const now = Date.now(); if (now - _lastSweep < 60000) return; _lastSweep = now const cut = now - Math.max(windowMs || 60000, 60000) for (const [k, arr] of buckets) { while (arr.length && arr[0] < cut) arr.shift(); if (!arr.length) buckets.delete(k) } } module.exports = { limited, clientIp, maybeSweep }