import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; // Lazy singleton. Module-load init would throw in environments without // Upstash env vars (local dev pre-onboarding, tests that transitively // import the auth handlers, Vercel build-time bundling). Defer construction // until the first request actually arrives. let cached = null; function init() { // Env-var names match Vercel's Upstash Marketplace integration, which // auto-provisions KV_REST_API_URL and KV_REST_API_TOKEN. See // https://upstash.com/docs/redis/howto/vercelintegration. Single-source- // of-truth — do NOT alias to UPSTASH_REDIS_REST_*. const url = process.env.KV_REST_API_URL; const token = process.env.KV_REST_API_TOKEN; if (url && token) { const redis = new Redis({ url, token }); const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, '15 m'), prefix: 'tcgvault:auth', }); return { mode: 'live', ratelimit }; } if (process.env.NODE_ENV === 'production') { // Fail-closed in production. A single failed login is a better outcome // than silently disabling brute-force protection on the live site. throw new Error( '[rate-limit] Upstash not configured. Set KV_REST_API_URL and KV_REST_API_TOKEN in the deployment environment (auto-provisioned by the Vercel Upstash Marketplace integration) before serving auth traffic.' ); } console.warn( '[rate-limit] KV_REST_API_URL / KV_REST_API_TOKEN not set — rate limiting disabled (dev/test only)' ); return { mode: 'noop' }; } function extractIdentifier(req) { const xff = req.headers?.['x-forwarded-for']; const firstHop = Array.isArray(xff) ? xff[0] : xff?.split(',')[0]?.trim(); return firstHop || req.socket?.remoteAddress || 'anonymous'; } export async function checkAuthRateLimit(req) { if (!cached) { cached = init(); } if (cached.mode === 'noop') { return { allowed: true, remaining: Infinity, reset: 0 }; } const identifier = extractIdentifier(req); try { const { success, remaining, reset } = await cached.ratelimit.limit(identifier); return { allowed: success, remaining, reset }; } catch (err) { // Fail-open on Upstash outage. A hard outage at the rate-limit backend // should not lock the entire user base out of login. Brute-force // protection lives behind defense-in-depth (Vercel firewall, etc.). console.error('[rate-limit]', err); return { allowed: true, remaining: Infinity, reset: 0 }; } }