/** * Rate Limiter para ambiente serverless * Utiliza Map em memória com TTL (Time To Live) * Adequado para serverless de curta duração */ interface RateLimitConfig { maxRequests: number; windowMs: number; } interface RateLimitEntry { count: number; resetAt: number; } // Cache em memória de requisições por IP const requestCache = new Map(); // Configuração de rate limits por endpoint const RATE_LIMITS: Record = { "/api/donations/create": { maxRequests: 5, windowMs: 10 * 60 * 1000, // 10 minutos }, "/api/donations/status": { maxRequests: 30, windowMs: 60 * 1000, // 1 minuto }, "/api/donations/verify": { maxRequests: 20, windowMs: 60 * 1000, // 1 minuto }, "/api/webhooks/abacatepay": { maxRequests: 100, windowMs: 60 * 1000, // 1 minuto }, "/api/auth/register": { maxRequests: 5, windowMs: 10 * 60 * 1000, // 10 minutos }, "/api/auth/login": { maxRequests: 10, windowMs: 60 * 1000, // 1 minuto }, "/api/auth/refresh": { maxRequests: 20, windowMs: 60 * 1000, // 1 minuto }, "/api/auth/me": { maxRequests: 30, windowMs: 60 * 1000, // 1 minuto }, }; // Limpar cache expirado a cada 5 minutos setInterval( () => { const now = Date.now(); let cleaned = 0; for (const [key, entry] of requestCache.entries()) { if (now > entry.resetAt) { requestCache.delete(key); cleaned++; } } if (cleaned > 0) { console.log(`🧹 Rate limiter: ${cleaned} entradas expiradas removidas`); } }, 5 * 60 * 1000, ); /** * Verifica se uma requisição excede o rate limit * @param ip - Endereço IP do cliente * @param path - Caminho da requisição (ex: /api/donations/create) * @returns Objeto com informações sobre o rate limit */ export function checkRateLimit( ip: string, path: string, ): { allowed: boolean; remaining: number; resetAt: number; limit: number; } { const config = RATE_LIMITS[path]; // Se não há config para este path, permite tudo if (!config) { return { allowed: true, remaining: Infinity, resetAt: Date.now() + 60000, limit: Infinity, }; } const key = `${ip}:${path}`; const now = Date.now(); const entry = requestCache.get(key); // Se não existe entrada ou expirou, criar nova if (!entry || now > entry.resetAt) { const newEntry: RateLimitEntry = { count: 1, resetAt: now + config.windowMs, }; requestCache.set(key, newEntry); return { allowed: true, remaining: config.maxRequests - 1, resetAt: newEntry.resetAt, limit: config.maxRequests, }; } // Se existe e não expirou, incrementar contador entry.count++; if (entry.count > config.maxRequests) { return { allowed: false, remaining: 0, resetAt: entry.resetAt, limit: config.maxRequests, }; } return { allowed: true, remaining: config.maxRequests - entry.count, resetAt: entry.resetAt, limit: config.maxRequests, }; } /** * Extrai o IP real de uma requisição * Considera headers de proxies/CDN (x-forwarded-for, x-real-ip) */ export function getClientIP(request: Request): string { // Tentar x-forwarded-for primeiro (pode ter múltiplos IPs) const forwardedFor = request.headers.get("x-forwarded-for"); if (forwardedFor) { // Pegar apenas o primeiro IP (cliente original) return forwardedFor.split(",")[0].trim(); } // Tentar x-real-ip const realIP = request.headers.get("x-real-ip"); if (realIP) { return realIP.trim(); } // Fallback: usar conexão remota return request.headers.get("cf-connecting-ip") || "unknown"; } /** * Middleware helper para criar resposta de rate limit */ export function createRateLimitResponse(resetAt: number): Response { const retryAfter = Math.ceil((resetAt - Date.now()) / 1000); return new Response( JSON.stringify({ error: "Rate limit excedido", message: "Muitas requisições. Tente novamente mais tarde.", retryAfter, }), { status: 429, headers: { "Content-Type": "application/json", "Retry-After": retryAfter.toString(), "X-RateLimit-Limit": "1", "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": new Date(resetAt).toISOString(), }, }, ); }