/** * Rate Limit Monitor * * Periodically checks rate limit metrics against configured thresholds * and triggers alerts when limits are exceeded. * * Designed to run as a cron job every 5 minutes. */ import { createClient } from 'jsr:@supabase/supabase-js@2'; import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; import { edgeLogger } from '../_shared/logger.ts'; import { getMetricsStats } from '../_shared/rateLimitMetrics.ts'; interface AlertConfig { id: string; metric_type: 'block_rate' | 'total_requests' | 'unique_ips' | 'function_specific'; threshold_value: number; time_window_ms: number; function_name?: string; enabled: boolean; } interface AlertCheck { configId: string; metricType: string; metricValue: number; thresholdValue: number; timeWindowMs: number; functionName?: string; exceeded: boolean; message: string; } async function checkAlertConditions(configs: AlertConfig[]): Promise { const checks: AlertCheck[] = []; for (const config of configs) { if (!config.enabled) continue; const stats = getMetricsStats(config.time_window_ms); let metricValue = 0; let exceeded = false; let message = ''; switch (config.metric_type) { case 'block_rate': metricValue = stats.blockRate; exceeded = metricValue > config.threshold_value; message = `Rate limit block rate (${(metricValue * 100).toFixed(1)}%) exceeded threshold (${(config.threshold_value * 100).toFixed(1)}%) in last ${config.time_window_ms / 1000}s. ${stats.blockedRequests} of ${stats.totalRequests} requests blocked.`; break; case 'total_requests': metricValue = stats.totalRequests; exceeded = metricValue > config.threshold_value; message = `Total requests (${metricValue}) exceeded threshold (${config.threshold_value}) in last ${config.time_window_ms / 1000}s.`; break; case 'unique_ips': metricValue = stats.uniqueIPs; exceeded = metricValue > config.threshold_value; message = `Unique IPs (${metricValue}) exceeded threshold (${config.threshold_value}) in last ${config.time_window_ms / 1000}s. Possible DDoS attack.`; break; case 'function_specific': // For function-specific alerts, we'd need to track metrics per function // This would require enhancing the metrics system console.log('Function-specific alerts not yet implemented'); continue; } checks.push({ configId: config.id, metricType: config.metric_type, metricValue, thresholdValue: config.threshold_value, timeWindowMs: config.time_window_ms, functionName: config.function_name, exceeded, message, }); } return checks; } async function recordAlert( supabase: any, check: AlertCheck ): Promise<{ success: boolean; error?: string }> { try { const { error } = await supabase .from('rate_limit_alerts') .insert({ config_id: check.configId, metric_type: check.metricType, metric_value: check.metricValue, threshold_value: check.thresholdValue, time_window_ms: check.timeWindowMs, function_name: check.functionName, alert_message: check.message, }); if (error) { console.error('Failed to record alert:', error); return { success: false, error: error.message }; } return { success: true }; } catch (error) { console.error('Exception recording alert:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async function sendNotification( supabase: any, check: AlertCheck ): Promise<{ success: boolean; error?: string }> { try { // Send notification to moderators via the moderator topic const { data, error } = await supabase.functions.invoke('trigger-notification', { body: { workflowId: 'rate-limit-alert', topicKey: 'moderators', payload: { message: check.message, metricType: check.metricType, metricValue: check.metricValue, thresholdValue: check.thresholdValue, functionName: check.functionName || 'all', }, overrides: { email: { subject: '🚨 Rate Limit Alert', }, }, }, }); if (error) { console.error('Failed to send notification:', error); return { success: false, error: error.message }; } return { success: true }; } catch (error) { console.error('Exception sending notification:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } export default createEdgeFunction( { name: 'monitor-rate-limits', requireAuth: false, }, async (req, context, supabase) => { const startTime = Date.now(); edgeLogger.info('Rate limit monitor starting', { requestId: context.requestId }); // Fetch enabled alert configurations const { data: configs, error: configError } = await supabase .from('rate_limit_alert_config') .select('*') .eq('enabled', true); if (configError) { edgeLogger.error('Failed to fetch alert configs', { error: configError, requestId: context.requestId }); throw configError; } if (!configs || configs.length === 0) { edgeLogger.info('No enabled alert configurations found', { requestId: context.requestId }); return new Response( JSON.stringify({ success: true, message: 'No enabled alert configurations', checked: 0 }), { headers: { 'Content-Type': 'application/json' } } ); } edgeLogger.info('Checking alert configurations', { count: configs.length, requestId: context.requestId }); // Check all alert conditions const checks = await checkAlertConditions(configs); const exceededChecks = checks.filter(c => c.exceeded); edgeLogger.info('Threshold violations found', { count: exceededChecks.length, requestId: context.requestId }); // Process exceeded thresholds const alertResults = []; for (const check of exceededChecks) { edgeLogger.info('Processing alert', { message: check.message, requestId: context.requestId }); // Check if we've already sent a recent alert for this config const { data: recentAlerts } = await supabase .from('rate_limit_alerts') .select('created_at') .eq('config_id', check.configId) .is('resolved_at', null) .gte('created_at', new Date(Date.now() - 15 * 60 * 1000).toISOString()) // Last 15 minutes .order('created_at', { ascending: false }) .limit(1); if (recentAlerts && recentAlerts.length > 0) { edgeLogger.info('Skipping alert - recent unresolved alert exists', { configId: check.configId, requestId: context.requestId }); alertResults.push({ configId: check.configId, skipped: true, reason: 'Recent alert exists', }); continue; } // Record the alert const recordResult = await recordAlert(supabase, check); // Send notification const notifyResult = await sendNotification(supabase, check); alertResults.push({ configId: check.configId, metricType: check.metricType, recorded: recordResult.success, notified: notifyResult.success, recordError: recordResult.error, notifyError: notifyResult.error, }); } const duration = Date.now() - startTime; edgeLogger.info('Monitor completed', { duration, requestId: context.requestId }); return new Response( JSON.stringify({ success: true, checked: configs.length, exceeded: exceededChecks.length, alerts: alertResults, duration_ms: duration, }), { headers: { 'Content-Type': 'application/json' } } ); } );