fix: critical P0+P1 issues for code quality score 9.0

P0 (Critical):
- api.ts: Add transaction rollback on INSERT failure in /api/deposit/deduct
  - Restores balance if transaction record fails to insert
  - Logs rollback success/failure for audit trail
  - Maintains data consistency despite D1's non-transactional nature

P1 (Important):
- summary-service.ts: Replace double type assertions with Type Guards
  - Add D1BufferedMessageRow, D1SummaryRow interfaces
  - Add isBufferedMessageRow, isSummaryRow type guards
  - Runtime validation with compile-time type safety
  - Remove all `as unknown as` patterns

- webhook.ts: Add integer range validation for callback queries
  - Add parseIntSafe() utility with min/max bounds
  - Validate domain registration price (0-10,000,000 KRW)
  - Prevent negative/overflow/NaN injection attacks

- search-tool.ts: Implement KV caching for translation API
  - Cache Korean→English translations for 24 hours
  - Use RATE_LIMIT_KV namespace with 'translate:' prefix
  - Reduce redundant OpenAI API calls for repeated queries

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-19 23:59:18 +09:00
parent 1708d78526
commit 97d6aa2850
4 changed files with 161 additions and 35 deletions

View File

@@ -3,6 +3,53 @@ import { createLogger } from './utils/logger';
const logger = createLogger('summary-service');
// Type Guards for D1 query results
interface D1BufferedMessageRow {
id: number;
role: string;
message: string;
created_at: string;
}
interface D1SummaryRow {
id: number;
generation: number;
summary: string;
message_count: number;
created_at: string;
}
function isBufferedMessageRow(item: unknown): item is D1BufferedMessageRow {
if (typeof item !== 'object' || item === null) return false;
const row = item as Record<string, unknown>;
return (
typeof row.id === 'number' &&
typeof row.role === 'string' &&
typeof row.message === 'string' &&
typeof row.created_at === 'string'
);
}
function isBufferedMessageArray(data: unknown): data is D1BufferedMessageRow[] {
return Array.isArray(data) && data.every(isBufferedMessageRow);
}
function isSummaryRow(item: unknown): item is D1SummaryRow {
if (typeof item !== 'object' || item === null) return false;
const row = item as Record<string, unknown>;
return (
typeof row.id === 'number' &&
typeof row.generation === 'number' &&
typeof row.summary === 'string' &&
typeof row.message_count === 'number' &&
typeof row.created_at === 'string'
);
}
function isSummaryArray(data: unknown): data is D1SummaryRow[] {
return Array.isArray(data) && data.every(isSummaryRow);
}
// 설정값 가져오기
const getConfig = (env: Env) => ({
summaryThreshold: parseInt(env.SUMMARY_THRESHOLD || '20', 10),
@@ -49,7 +96,20 @@ export async function getBufferedMessages(
.bind(userId, chatId)
.all();
return (results || []) as unknown as BufferedMessage[];
if (!isBufferedMessageArray(results)) {
logger.warn('Invalid message buffer data format', { userId, chatId });
return [];
}
// Type narrowing ensures results is D1BufferedMessageRow[]
const validatedResults: D1BufferedMessageRow[] = results;
return validatedResults.map(row => ({
id: row.id,
role: row.role as 'user' | 'bot',
message: row.message,
created_at: row.created_at
}));
}
// 최신 요약 조회
@@ -89,7 +149,15 @@ export async function getAllSummaries(
.bind(userId, chatId)
.all();
return (results || []) as unknown as Summary[];
if (!isSummaryArray(results)) {
logger.warn('Invalid summaries data format', { userId, chatId });
return [];
}
// Type narrowing ensures results is D1SummaryRow[] which matches Summary[]
const validatedResults: D1SummaryRow[] = results;
return validatedResults;
}
// 전체 컨텍스트 조회