fix: address remaining code review issues

- Apply sanitizeForAIPrompt to AI prompt (prevent prompt injection)
- Replace hardcoded provider IDs with name-based filtering
- Remove dead code (queryVPSBenchmarks function)
- Use LIMITS.MAX_REQUEST_BODY_BYTES constant
- Change parseAIResponse parameter from `any` to `unknown`

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 18:15:09 +09:00
parent 7dfd3659ec
commit 4bed3237fc
2 changed files with 136 additions and 68 deletions

View File

@@ -14,7 +14,7 @@ import type {
BenchmarkReference,
AIRecommendationResponse
} from '../types';
import { i18n } from '../config';
import { i18n, LIMITS } from '../config';
import {
jsonResponse,
validateRecommendRequest,
@@ -27,6 +27,7 @@ import {
isValidVPSBenchmark,
isValidTechSpec,
isValidAIRecommendation,
sanitizeForAIPrompt,
DEFAULT_REGION_FILTER_SQL
} from '../utils';
@@ -40,7 +41,7 @@ export async function handleRecommend(
try {
// Check request body size to prevent large payload attacks
const contentLength = request.headers.get('Content-Length');
if (contentLength && parseInt(contentLength, 10) > 10240) { // 10KB limit
if (contentLength && parseInt(contentLength, 10) > LIMITS.MAX_REQUEST_BODY_BYTES) {
return jsonResponse(
{ error: 'Request body too large', max_size: '10KB' },
413,
@@ -52,7 +53,7 @@ export async function handleRecommend(
const bodyText = await request.text();
const actualBodySize = new TextEncoder().encode(bodyText).length;
if (actualBodySize > 10240) { // 10KB limit
if (actualBodySize > LIMITS.MAX_REQUEST_BODY_BYTES) {
return jsonResponse(
{ error: 'Request body too large', max_size: '10KB', actual_size: actualBodySize },
413,
@@ -345,7 +346,7 @@ async function queryCandidateServers(
JOIN providers p ON it.provider_id = p.id
JOIN pricing pr ON pr.instance_type_id = it.id
JOIN regions r ON pr.region_id = r.id
WHERE p.id IN (1, 2) -- Linode, Vultr only
WHERE LOWER(p.name) IN ('linode', 'vultr') -- Linode, Vultr only
`;
const params: (string | number)[] = [];
@@ -442,10 +443,10 @@ async function queryCandidateServers(
}
// Group by instance + region to show each server per region
// For heavy/very_heavy bandwidth, prioritize Linode (p.id=1) due to generous bandwidth allowance
// For heavy/very_heavy bandwidth, prioritize Linode due to generous bandwidth allowance
const isHighBandwidth = bandwidthEstimate?.category === 'heavy' || bandwidthEstimate?.category === 'very_heavy';
const orderByClause = isHighBandwidth
? `ORDER BY CASE WHEN p.id = 1 THEN 0 ELSE 1 END, monthly_price ASC`
? `ORDER BY CASE WHEN LOWER(p.name) = 'linode' THEN 0 ELSE 1 END, monthly_price ASC`
: `ORDER BY monthly_price ASC`;
query += ` GROUP BY it.id, r.id ${orderByClause} LIMIT 50`;
@@ -601,59 +602,6 @@ function getBenchmarkReference(
};
}
/**
* Query VPS benchmarks - prioritize matching provider
*/
async function queryVPSBenchmarks(
db: D1Database,
vcpu: number,
memoryGb: number,
providerHint?: string
): Promise<VPSBenchmark[]> {
const vcpuMin = Math.max(1, vcpu - 1);
const vcpuMax = vcpu + 2;
const memMin = Math.max(1, memoryGb - 2);
const memMax = memoryGb + 4;
// First try to find benchmarks from the same provider
if (providerHint) {
const providerQuery = `
SELECT *
FROM vps_benchmarks
WHERE (LOWER(provider_name) LIKE ? ESCAPE '\\' OR LOWER(plan_name) LIKE ? ESCAPE '\\')
ORDER BY gb6_single_normalized DESC
LIMIT 20
`;
const escapedHint = escapeLikePattern(providerHint.toLowerCase());
const providerPattern = `%${escapedHint}%`;
const providerResult = await db.prepare(providerQuery).bind(providerPattern, providerPattern).all();
if (providerResult.success && providerResult.results.length > 0) {
// Validate each result with type guard
return (providerResult.results as unknown[]).filter(isValidVPSBenchmark);
}
}
// Fallback: Find VPS with similar specs
const query = `
SELECT *
FROM vps_benchmarks
WHERE vcpu >= ? AND vcpu <= ?
AND memory_gb >= ? AND memory_gb <= ?
ORDER BY gb6_single_normalized DESC
LIMIT 10
`;
const result = await db.prepare(query).bind(vcpuMin, vcpuMax, memMin, memMax).all();
if (!result.success) {
return [];
}
// Validate each result with type guard
return (result.results as unknown[]).filter(isValidVPSBenchmark);
}
/**
* Query VPS benchmarks in a single batched query
* Consolidates multiple provider-specific queries into one for better performance
@@ -930,13 +878,17 @@ ${languageInstruction}`;
console.log(`[AI] Filtered ${candidates.length} candidates to ${topCandidates.length} for AI analysis`);
// Sanitize user inputs to prevent prompt injection
const sanitizedTechStack = req.tech_stack.map(t => sanitizeForAIPrompt(t, 50)).join(', ');
const sanitizedUseCase = sanitizeForAIPrompt(req.use_case, 200);
const userPrompt = `Analyze these server options and recommend the top 3 best matches.
## User Requirements
- Tech Stack: ${req.tech_stack.join(', ')}
- Tech Stack: ${sanitizedTechStack}
- Expected Concurrent Users: ${req.expected_users} ${req.traffic_pattern === 'spiky' ? '(with traffic spikes)' : req.traffic_pattern === 'growing' ? '(growing user base)' : '(steady traffic)'}
- **Estimated DAU (Daily Active Users)**: ${bandwidthEstimate.estimated_dau_min.toLocaleString()}-${bandwidthEstimate.estimated_dau_max.toLocaleString()}명 (동시 접속 ${req.expected_users}명 기준)
- Use Case: ${req.use_case}
- Use Case: ${sanitizedUseCase}
- Traffic Pattern: ${req.traffic_pattern || 'steady'}
- **Estimated Monthly Bandwidth**: ${bandwidthEstimate.monthly_tb >= 1 ? `${bandwidthEstimate.monthly_tb} TB` : `${bandwidthEstimate.monthly_gb} GB`} (${bandwidthEstimate.category})
${isHighTraffic ? `- ⚠️ HIGH BANDWIDTH WORKLOAD (${bandwidthEstimate.monthly_tb} TB/month): MUST recommend Linode over Vultr. Linode includes 1-6TB/month transfer vs Vultr overage charges ($0.01/GB). Bandwidth cost savings > base price difference.` : ''}
@@ -1182,19 +1134,38 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have
/**
* Parse AI response and extract JSON
*/
function parseAIResponse(response: any): AIRecommendationResponse {
function parseAIResponse(response: unknown): AIRecommendationResponse {
try {
// Handle different response formats
let content: string;
if (typeof response === 'string') {
content = response;
} else if (response.response) {
content = response.response;
} else if (response.result && response.result.response) {
content = response.result.response;
} else if (response.choices && response.choices[0]?.message?.content) {
content = response.choices[0].message.content;
} else if (typeof response === 'object' && response !== null) {
// Type guard for response object with different structures
const resp = response as Record<string, unknown>;
if (typeof resp.response === 'string') {
content = resp.response;
} else if (typeof resp.result === 'object' && resp.result !== null) {
const result = resp.result as Record<string, unknown>;
if (typeof result.response === 'string') {
content = result.response;
} else {
throw new Error('Unexpected AI response format');
}
} else if (Array.isArray(resp.choices) && resp.choices.length > 0) {
const choice = resp.choices[0] as Record<string, unknown>;
const message = choice?.message as Record<string, unknown>;
if (typeof message?.content === 'string') {
content = message.content;
} else {
throw new Error('Unexpected AI response format');
}
} else {
console.error('[AI] Unexpected response format:', response);
throw new Error('Unexpected AI response format');
}
} else {
console.error('[AI] Unexpected response format:', response);
throw new Error('Unexpected AI response format');