From 502bbd271ecdf66453903fe6e73173f3a9fec615 Mon Sep 17 00:00:00 2001 From: kappa Date: Sun, 25 Jan 2026 14:28:09 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=20=EC=95=88?= =?UTF-8?q?=EC=A0=84=EC=84=B1=20=EB=B0=8F=20=EC=95=88=EC=A0=95=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB 결과 타입 검증용 type guard 함수 추가 (isValidServer, isValidVPSBenchmark, isValidTechSpec, isValidBenchmarkData, isValidAIRecommendation) - 모든 DB 쿼리 결과에 타입 가드 적용하여 런타임 검증 - AI 응답 파싱에 구조 검증 추가 - OpenAI API 호출에 30초 타임아웃 추가 (AbortController) - 타임아웃 에러 처리 개선 Co-Authored-By: Claude Opus 4.5 --- src/index.ts | 138 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 29dccc6..b4095f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -626,8 +626,12 @@ async function handleGetServers( throw new Error('Database query failed'); } - // NOTE: Type assertion without validation - should implement proper validation - const servers = result.results as unknown as Server[]; + // Validate each result with type guard + const servers = (result.results as unknown[]).filter(isValidServer); + const invalidCount = result.results.length - servers.length; + if (invalidCount > 0) { + console.warn(`[GetServers] Filtered out ${invalidCount} invalid server records`); + } console.log('[GetServers] Found servers:', servers.length); @@ -819,6 +823,80 @@ async function handleRecommend( } } +/** + * Type guard to validate Server object structure + */ +function isValidServer(obj: unknown): obj is Server { + if (!obj || typeof obj !== 'object') return false; + const s = obj as Record; + return ( + typeof s.id === 'number' && + typeof s.provider_name === 'string' && + typeof s.instance_id === 'string' && + typeof s.vcpu === 'number' && + typeof s.memory_mb === 'number' && + typeof s.monthly_price === 'number' + ); +} + +/** + * Type guard to validate VPSBenchmark object structure + */ +function isValidVPSBenchmark(obj: unknown): obj is VPSBenchmark { + if (!obj || typeof obj !== 'object') return false; + const v = obj as Record; + return ( + typeof v.id === 'number' && + typeof v.provider_name === 'string' && + typeof v.vcpu === 'number' && + typeof v.geekbench_single === 'number' + ); +} + +/** + * Type guard to validate TechSpec object structure + */ +function isValidTechSpec(obj: unknown): obj is TechSpec { + if (!obj || typeof obj !== 'object') return false; + const t = obj as Record; + return ( + typeof t.id === 'number' && + typeof t.name === 'string' && + typeof t.vcpu_per_users === 'number' && + typeof t.min_memory_mb === 'number' + ); +} + +/** + * Type guard to validate BenchmarkData object structure + */ +function isValidBenchmarkData(obj: unknown): obj is BenchmarkData { + if (!obj || typeof obj !== 'object') return false; + const b = obj as Record; + return ( + typeof b.id === 'number' && + typeof b.processor_name === 'string' && + typeof b.benchmark_name === 'string' && + typeof b.score === 'number' + ); +} + +/** + * Type guard to validate AI recommendation structure + */ +function isValidAIRecommendation(obj: unknown): obj is AIRecommendationResponse['recommendations'][0] { + if (!obj || typeof obj !== 'object') return false; + const r = obj as Record; + return ( + (typeof r.server_id === 'number' || typeof r.server_id === 'string') && + typeof r.score === 'number' && + r.analysis !== null && + typeof r.analysis === 'object' && + r.estimated_capacity !== null && + typeof r.estimated_capacity === 'object' + ); +} + /** * Validate recommendation request */ @@ -1093,8 +1171,13 @@ async function queryCandidateServers( throw new Error('Failed to query candidate servers'); } - // NOTE: Type assertion without validation - should implement proper validation - return result.results as unknown as Server[]; + // Validate each result with type guard + const validServers = (result.results as unknown[]).filter(isValidServer); + const invalidCount = result.results.length - validServers.length; + if (invalidCount > 0) { + console.warn(`[Candidates] Filtered out ${invalidCount} invalid server records`); + } + return validServers; } /** @@ -1174,8 +1257,8 @@ async function queryBenchmarkData( return []; } - // NOTE: Type assertion without validation - should implement proper validation - return result.results as unknown as BenchmarkData[]; + // Validate each result with type guard + return (result.results as unknown[]).filter(isValidBenchmarkData); } /** @@ -1256,8 +1339,8 @@ async function queryVPSBenchmarks( const providerResult = await db.prepare(providerQuery).bind(providerPattern, providerPattern).all(); if (providerResult.success && providerResult.results.length > 0) { - // NOTE: Type assertion without validation - should implement proper validation - return providerResult.results as unknown as VPSBenchmark[]; + // Validate each result with type guard + return (providerResult.results as unknown[]).filter(isValidVPSBenchmark); } } @@ -1277,8 +1360,8 @@ async function queryVPSBenchmarks( return []; } - // NOTE: Type assertion without validation - should implement proper validation - return result.results as unknown as VPSBenchmark[]; + // Validate each result with type guard + return (result.results as unknown[]).filter(isValidVPSBenchmark); } /** @@ -1325,8 +1408,8 @@ async function queryVPSBenchmarksBatch( return []; } - // NOTE: Type assertion without validation - should implement proper validation - return result.results as unknown as VPSBenchmark[]; + // Validate each result with type guard + return (result.results as unknown[]).filter(isValidVPSBenchmark); } /** @@ -1425,8 +1508,10 @@ async function queryTechSpecs( return []; } - console.log(`[TechSpecs] Found ${result.results.length} specs for: ${normalizedStack.join(', ')}`); - return result.results as unknown as TechSpec[]; + // Validate each result with type guard + const validSpecs = (result.results as unknown[]).filter(isValidTechSpec); + console.log(`[TechSpecs] Found ${validSpecs.length} specs for: ${normalizedStack.join(', ')}`); + return validSpecs; } catch (error) { console.error('[TechSpecs] Error:', error); return []; @@ -1604,6 +1689,10 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have console.log('[AI] Sending request to OpenAI GPT-4o-mini'); + // Create AbortController with 30 second timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); + try { const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', @@ -1620,8 +1709,11 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have max_tokens: 2000, temperature: 0.3, }), + signal: controller.signal, }); + clearTimeout(timeoutId); + if (!openaiResponse.ok) { const errorText = await openaiResponse.text(); const sanitized = errorText.slice(0, 100).replace(/sk-[a-zA-Z0-9-_]+/g, 'sk-***'); @@ -1716,6 +1808,12 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have infrastructure_tips: aiResult.infrastructure_tips, }; } catch (error) { + clearTimeout(timeoutId); + // Handle timeout specifically + if (error instanceof Error && error.name === 'AbortError') { + console.error('[AI] Request timed out after 30 seconds'); + throw new Error('AI request timed out - please try again'); + } console.error('[AI] Error:', error); throw new Error(`AI processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -1757,7 +1855,17 @@ function parseAIResponse(response: any): AIRecommendationResponse { throw new Error('Invalid recommendations structure'); } - return parsed as AIRecommendationResponse; + // Validate each recommendation with type guard + const validRecommendations = parsed.recommendations.filter(isValidAIRecommendation); + if (validRecommendations.length === 0 && parsed.recommendations.length > 0) { + console.warn('[AI] All recommendations failed validation, raw:', JSON.stringify(parsed.recommendations[0]).slice(0, 200)); + throw new Error('AI recommendations failed validation'); + } + + return { + recommendations: validRecommendations, + infrastructure_tips: Array.isArray(parsed.infrastructure_tips) ? parsed.infrastructure_tips : [], + } as AIRecommendationResponse; } catch (error) { console.error('[AI] Parse error:', error); console.error('[AI] Response was:', response);