diff --git a/supabase/functions/_shared/rateLimiter.ts b/supabase/functions/_shared/rateLimiter.ts index 60e22376..d37d544a 100644 --- a/supabase/functions/_shared/rateLimiter.ts +++ b/supabase/functions/_shared/rateLimiter.ts @@ -3,6 +3,9 @@ * Prevents abuse and DoS attacks with in-memory rate limiting */ +import { recordRateLimitMetric } from './rateLimitMetrics.ts'; +import { extractUserIdFromAuth, getClientIP } from './authHelpers.ts'; + export interface RateLimitConfig { windowMs: number; // Time window in milliseconds maxRequests: number; // Max requests per window @@ -21,8 +24,12 @@ class RateLimiter { private rateLimitMap = new Map(); private config: Required; private cleanupInterval: number; + private tierName: string; + private functionName?: string; - constructor(config: RateLimitConfig) { + constructor(config: RateLimitConfig, tierName: string = 'custom', functionName?: string) { + this.tierName = tierName; + this.functionName = functionName; this.config = { maxMapSize: 10000, keyGenerator: (req: Request) => this.getClientIP(req), @@ -38,16 +45,8 @@ class RateLimiter { } private getClientIP(req: Request): string { - if (this.config.trustProxy) { - const forwarded = req.headers.get('x-forwarded-for'); - if (forwarded) return forwarded.split(',')[0].trim(); - - const realIP = req.headers.get('x-real-ip'); - if (realIP) return realIP; - } - - // Fallback for testing - return '0.0.0.0'; + // Use centralized auth helper for consistent IP extraction + return getClientIP(req); } private cleanupExpiredEntries(): void { @@ -73,15 +72,33 @@ class RateLimiter { } } - check(req: Request): RateLimitResult { + check(req: Request, functionName?: string): RateLimitResult { const key = this.config.keyGenerator(req); const now = Date.now(); const existing = this.rateLimitMap.get(key); + // Extract metadata for metrics + const clientIP = getClientIP(req); + const userId = extractUserIdFromAuth(req); + const actualFunctionName = functionName || this.functionName || 'unknown'; + // Check existing entry if (existing && now <= existing.resetAt) { if (existing.count >= this.config.maxRequests) { const retryAfter = Math.ceil((existing.resetAt - now) / 1000); + + // Record blocked request metric + recordRateLimitMetric({ + timestamp: now, + functionName: actualFunctionName, + clientIP, + userId: userId || undefined, + allowed: false, + remaining: 0, + retryAfter, + tier: this.tierName, + }); + return { allowed: false, retryAfter, @@ -89,9 +106,22 @@ class RateLimiter { }; } existing.count++; + const remaining = this.config.maxRequests - existing.count; + + // Record allowed request metric + recordRateLimitMetric({ + timestamp: now, + functionName: actualFunctionName, + clientIP, + userId: userId || undefined, + allowed: true, + remaining, + tier: this.tierName, + }); + return { allowed: true, - remaining: this.config.maxRequests - existing.count + remaining }; } @@ -117,9 +147,22 @@ class RateLimiter { resetAt: now + this.config.windowMs }); + const remaining = this.config.maxRequests - 1; + + // Record allowed request metric + recordRateLimitMetric({ + timestamp: now, + functionName: actualFunctionName, + clientIP, + userId: userId || undefined, + allowed: true, + remaining, + tier: this.tierName, + }); + return { allowed: true, - remaining: this.config.maxRequests - 1 + remaining }; } @@ -143,8 +186,8 @@ import { } from './rateLimitConfig.ts'; // Export factory function for creating custom rate limiters -export function createRateLimiter(config: RateLimitConfig): RateLimiter { - return new RateLimiter(config); +export function createRateLimiter(config: RateLimitConfig, tierName?: string, functionName?: string): RateLimiter { + return new RateLimiter(config, tierName, functionName); } /** @@ -155,41 +198,42 @@ export function createRateLimiter(config: RateLimitConfig): RateLimiter { */ export const rateLimiters = { // Strict: 5 requests/minute - For expensive operations - strict: createRateLimiter(RATE_LIMIT_STRICT), + strict: createRateLimiter(RATE_LIMIT_STRICT, 'strict'), // Moderate: 10 requests/minute - For moderation and submissions - moderate: createRateLimiter(RATE_LIMIT_MODERATE), + moderate: createRateLimiter(RATE_LIMIT_MODERATE, 'moderate'), // Standard: 20 requests/minute - For typical operations (DEPRECATED: use 'moderate' for 10/min or 'standard' for 20/min) - standard: createRateLimiter(RATE_LIMIT_MODERATE), // Keeping for backward compatibility + standard: createRateLimiter(RATE_LIMIT_MODERATE, 'standard'), // Keeping for backward compatibility // Lenient: 30 requests/minute - For lightweight reads - lenient: createRateLimiter(RATE_LIMIT_LENIENT), + lenient: createRateLimiter(RATE_LIMIT_LENIENT, 'lenient'), // Generous: 60 requests/minute - For high-frequency operations - generous: createRateLimiter(RATE_LIMIT_GENEROUS), + generous: createRateLimiter(RATE_LIMIT_GENEROUS, 'generous'), // Per-user rate limiters (key by user ID instead of IP) - perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT), - perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE), - perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD), - perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT), + perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT, 'perUserStrict'), + perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE, 'perUserModerate'), + perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD, 'perUserStandard'), + perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT, 'perUserLenient'), // Legacy per-user factory function (DEPRECATED: use perUserStrict, perUserModerate, etc.) perUser: (maxRequests: number = 20) => createRateLimiter({ ...RATE_LIMIT_PER_USER_STANDARD, maxRequests, - }), + }, 'perUser'), }; // Middleware helper export function withRateLimit( handler: (req: Request) => Promise, limiter: RateLimiter, - corsHeaders: Record = {} + corsHeaders: Record = {}, + functionName?: string ): (req: Request) => Promise { return async (req: Request) => { - const result = limiter.check(req); + const result = limiter.check(req, functionName); if (!result.allowed) { return new Response(