| // In-memory magic-link code store. Replace with Redis / DB for multi-instance deployments. | |
| export const pendingCodes = new Map< | |
| string, | |
| { code: string; expiresAt: number } | |
| >(); | |
| export const CODE_TTL_MS = 10 * 60 * 1000; // 10 minutes | |
| /** Per-email rate limit: max requests within the window. */ | |
| const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 min | |
| const RATE_LIMIT_MAX = 3; | |
| const rateLimitMap = new Map<string, { count: number; windowStart: number }>(); | |
| /** Returns true if the email has exceeded the rate limit. */ | |
| export function isRateLimited(email: string): boolean { | |
| const now = Date.now(); | |
| const entry = rateLimitMap.get(email); | |
| if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) { | |
| rateLimitMap.set(email, { count: 1, windowStart: now }); | |
| return false; | |
| } | |
| entry.count++; | |
| return entry.count > RATE_LIMIT_MAX; | |
| } | |
| /** Purge expired codes and stale rate-limit entries. Call periodically. */ | |
| export function purgeExpired(): void { | |
| const now = Date.now(); | |
| Array.from(pendingCodes.entries()).forEach(([email, entry]) => { | |
| if (now > entry.expiresAt) pendingCodes.delete(email); | |
| }); | |
| Array.from(rateLimitMap.entries()).forEach(([email, entry]) => { | |
| if (now - entry.windowStart > RATE_LIMIT_WINDOW_MS) rateLimitMap.delete(email); | |
| }); | |
| } | |