import { NextRequest, NextResponse } from 'next/server'; import { getOpenAIService, OpenAIServiceError } from '@/lib/services/claudeService'; import { parseAnalysisResponse, ParseError } from '@/lib/services/responseParser'; import { validateSectorInput } from '@/lib/utils/validation'; import { API_CONFIG } from '@/lib/constants/config'; /** * In-memory rate limiting store (simple implementation for MVP) * Maps IP address to array of request timestamps */ const rateLimitStore = new Map(); /** * Checks if request exceeds rate limit */ function checkRateLimit(ip: string): { allowed: boolean; retryAfter?: number } { const now = Date.now(); const windowStart = now - API_CONFIG.RATE_LIMIT_WINDOW_MS; // Get or create request timestamps for this IP const timestamps = rateLimitStore.get(ip) || []; // Remove old timestamps outside the window const recentTimestamps = timestamps.filter((t) => t > windowStart); if (recentTimestamps.length >= API_CONFIG.RATE_LIMIT_MAX_REQUESTS) { // Find the oldest request in the window const oldestTimestamp = Math.min(...recentTimestamps); const retryAfter = Math.ceil((oldestTimestamp + API_CONFIG.RATE_LIMIT_WINDOW_MS - now) / 1000); return { allowed: false, retryAfter }; } // Add current request timestamp recentTimestamps.push(now); rateLimitStore.set(ip, recentTimestamps); // Cleanup: remove old entries from store periodically if (Math.random() < 0.1) { for (const [key, times] of rateLimitStore.entries()) { const recentTimes = times.filter((t) => t > windowStart); if (recentTimes.length === 0) { rateLimitStore.delete(key); } else { rateLimitStore.set(key, recentTimes); } } } return { allowed: true }; } /** * Sends SSE event to client */ function sendSSEEvent( encoder: TextEncoder, eventType: string, data: unknown, ): Uint8Array { const jsonData = JSON.stringify(data); const event = `event: ${eventType}\ndata: ${jsonData}\n\n`; return encoder.encode(event); } /** * GET /api/analyze - SSE endpoint for competitive intelligence analysis * * Query params: * - sector: string (required) - sector name to analyze * * Returns SSE stream with events: * - progress: {message: string} * - data: {sector_overview, key_players, ...} * - complete: {} * - error: {error, code, retryAfter?} */ export async function GET(request: NextRequest) { const encoder = new TextEncoder(); // Create response with SSE headers const response = new NextResponse( new ReadableStream({ async start(controller) { try { // Extract and validate sector from query params const sector = request.nextUrl.searchParams.get('sector'); if (!sector) { const errorData = { error: 'sector parameter is required', code: 'MISSING_SECTOR' }; controller.enqueue(sendSSEEvent(encoder, 'error', errorData)); controller.close(); return; } // Validate sector input const validation = validateSectorInput(sector); if (!validation.valid) { const errorData = { error: validation.error, code: 'VALIDATION_ERROR' }; controller.enqueue(sendSSEEvent(encoder, 'error', errorData)); controller.close(); return; } // Check rate limiting const clientIP = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; const rateLimit = checkRateLimit(clientIP); if (!rateLimit.allowed) { const errorData = { error: `Too many requests. Try again in ${rateLimit.retryAfter} seconds`, code: 'RATE_LIMIT_ERROR', retryAfter: rateLimit.retryAfter, }; controller.enqueue(sendSSEEvent(encoder, 'error', errorData)); controller.close(); return; } // Set up timeout const timeoutId = setTimeout(() => { const errorData = { error: 'Request timeout. Please try again.', code: 'TIMEOUT_ERROR', }; controller.enqueue(sendSSEEvent(encoder, 'error', errorData)); controller.close(); }, API_CONFIG.REQUEST_TIMEOUT); // Initialize OpenAI service const openaiService = getOpenAIService(); // Progress callback const onProgress = (message: string) => { controller.enqueue(sendSSEEvent(encoder, 'progress', { message })); }; // Analyze sector const analysis = await openaiService.analyzeSector(validation.value!, onProgress); // Clear timeout clearTimeout(timeoutId); // Send data event controller.enqueue(sendSSEEvent(encoder, 'data', { payload: analysis })); // Send complete event controller.enqueue(sendSSEEvent(encoder, 'complete', {})); // Close stream controller.close(); } catch (error) { let errorCode = 'INTERNAL_ERROR'; let errorMessage = 'An unexpected error occurred'; let statusCode = 500; if (error instanceof OpenAIServiceError) { errorCode = error.code; errorMessage = error.message; if (error.code === 'RATE_LIMIT_ERROR') { statusCode = 429; } else if (error.code === 'INVALID_API_KEY') { statusCode = 500; // Don't expose auth errors to client errorMessage = 'Service misconfiguration. Please contact support.'; } } else if (error instanceof ParseError) { errorCode = error.code; errorMessage = 'Unable to parse analysis. Please try again with a different sector.'; statusCode = 500; } else if (error instanceof Error) { errorMessage = error.message; } const errorData = { error: errorMessage, code: errorCode, }; controller.enqueue(sendSSEEvent(encoder, 'error', errorData)); controller.close(); } }, }), { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'X-Accel-Buffering': 'no', // Disable Nginx buffering }, } ); return response; }