/** * Minimal in-memory fixed-window rate limiter for a single Node process * (this app always runs as one process — see server.ts). Good enough to * blunt abuse of the email-verification resend endpoint without adding an * external store; resets on deploy/restart, which is an acceptable * trade-off for this use case. */ const buckets = new Map(); // Opportunistic cleanup so long-running processes don't accumulate an // unbounded number of stale keys (one per distinct IP/email ever seen). const MAX_TRACKED_KEYS = 5000; function sweepExpired(now: number) { for (const [key, bucket] of buckets) { if (bucket.resetAt < now) buckets.delete(key); } } export function checkRateLimit(key: string, opts: { max: number; windowMs: number }): boolean { const now = Date.now(); if (buckets.size > MAX_TRACKED_KEYS) sweepExpired(now); const bucket = buckets.get(key); if (!bucket || bucket.resetAt < now) { buckets.set(key, { count: 1, resetAt: now + opts.windowMs }); return true; } if (bucket.count >= opts.max) return false; bucket.count += 1; return true; } export function getClientIp(request: Request): string { const forwarded = request.headers.get("x-forwarded-for"); if (forwarded) return forwarded.split(",")[0]!.trim(); return "unknown"; }