// A per-key sliding-window limiter for the two doors anyone may walk // through unseated: creating rooms and filing reports. Per-connection // caps reset on reconnect; these are keyed by client address and hold // for the window. Memory is bounded by pruning keys whose windows have // emptied. export class SlidingLimit { private hits = new Map(); private lastSweep = 0; constructor(private readonly max: number, private readonly windowMs: number) {} /** Record a hit for `key` if it is under the limit; false if it is not. */ allow(key: string, now = Date.now()): boolean { this.sweep(now); const cutoff = now - this.windowMs; const times = (this.hits.get(key) ?? []).filter((t) => t > cutoff); if (times.length >= this.max) { this.hits.set(key, times); return false; } times.push(now); this.hits.set(key, times); return true; } private sweep(now: number): void { if (now - this.lastSweep < this.windowMs) return; this.lastSweep = now; const cutoff = now - this.windowMs; for (const [k, times] of this.hits) { if (!times.some((t) => t > cutoff)) this.hits.delete(k); } } } const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]); /** The client's address as Caddy reports it. The proxy APPENDS the true * peer to X-Forwarded-For, so the last entry is the trustworthy one; a * client can write anything into the first. And the header is believed * only when the socket peer is the proxy itself (loopback) — reached * any other way, the peer address is the client. */ export function clientAddress(headers: Record, remote: string | undefined): string { const peer = remote || "unknown"; if (!LOOPBACK.has(peer)) return peer; const fwd = headers["x-forwarded-for"]; const parts = (Array.isArray(fwd) ? fwd.join(",") : fwd ?? "").split(",").map((s) => s.trim()).filter(Boolean); return parts[parts.length - 1] || peer; }