// Simple in-memory rate limiter (PRD §5.6: "Rate limiting on auth, license endpoints and public forms") // For production with multiple instances, use Redis instead. interface RateLimitEntry { count: number resetAt: number } const store = new Map() // Check + increment rate limit // Returns { allowed: boolean, remaining: number, resetAt: number } export function rateLimit( key: string, maxRequests: number = 10, windowMs: number = 60_000 ): { allowed: boolean; remaining: number; resetAt: number } { const now = Date.now() const entry = store.get(key) if (!entry || entry.resetAt < now) { // First request or window expired const resetAt = now + windowMs store.set(key, { count: 1, resetAt }) return { allowed: true, remaining: maxRequests - 1, resetAt } } if (entry.count >= maxRequests) { return { allowed: false, remaining: 0, resetAt: entry.resetAt } } entry.count++ return { allowed: true, remaining: maxRequests - entry.count, resetAt: entry.resetAt } } // Get client identifier from request (IP + path for scoping) export function getRateLimitKey(req: Request, scope: string): string { const headers = new Headers(req.headers) const ip = headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? headers.get('x-real-ip') ?? 'unknown' return `${scope}:${ip}` } // Rate limit configs per endpoint type (PRD §5.6) export const RATE_LIMITS = { auth: { max: 5, window: 60_000 }, // 5 attempts/minute (login, passkey) public: { max: 20, window: 60_000 }, // 20 requests/minute (public forms) api: { max: 100, window: 60_000 }, // 100 requests/minute (authenticated API) license: { max: 30, window: 60_000 }, // 30 requests/minute (license validate/activate) mcp: { max: 60, window: 60_000 }, // 60 requests/minute (MCP server) webhook: { max: 10, window: 60_000 }, // 10 triggers/minute (webhook test) } as const // Helper: check rate limit and return 429 Response if exceeded export function checkRateLimit(req: Request, scope: keyof typeof RATE_LIMITS): Response | null { const config = RATE_LIMITS[scope] const key = getRateLimitKey(req, scope) const result = rateLimit(key, config.max, config.window) if (!result.allowed) { const retryAfter = Math.ceil((result.resetAt - Date.now()) / 1000) return new Response( JSON.stringify({ error: 'rate_limit_exceeded', message: `Too many requests. Try again in ${retryAfter} seconds.`, retryAfter, }), { status: 429, headers: { 'content-type': 'application/json', 'retry-after': String(retryAfter), 'x-rate-limit-limit': String(config.max), 'x-rate-limit-remaining': '0', 'x-rate-limit-reset': String(result.resetAt), }, } ) } return null // allowed }