refactor: comprehensive code review fixes (security, performance, QA)

## Security Improvements
- Fix timing attack in verifyApiKey with fixed 256-byte buffer
- Fix sortOrder SQL injection with whitelist validation
- Fix rate limiting bypass for non-Cloudflare traffic (fail-closed)
- Remove stack trace exposure in error responses
- Add request_id for audit trail (X-Request-ID header)
- Sanitize origin header to prevent log injection
- Add content-length validation for /sync endpoint (10KB limit)
- Replace Math.random() with crypto.randomUUID() for sync IDs
- Expand sensitive data masking patterns (8 → 18)

## Performance Improvements
- Reduce rate limiter KV reads from 3 to 1 per request (66% reduction)
- Increase sync batch size from 100 to 500 (80% fewer batches)
- Fix health check N+1 query with efficient JOINs
- Fix COUNT(*) Cartesian product with COUNT(DISTINCT)
- Implement shared logger cache pattern across repositories
- Add CacheService singleton pattern in recommend.ts
- Add composite index for recommendation queries
- Implement Anvil pricing query batching (100 per chunk)

## QA Improvements
- Add BATCH_SIZE bounds validation (1-1000)
- Add pagination bounds (page >= 1, MAX_OFFSET = 100000)
- Add min/max range consistency validation
- Add DB reference validation for singleton services
- Add type guards for database result validation
- Add timeout mechanism for external API calls (10-60s)
- Use SUPPORTED_PROVIDERS constant instead of hardcoded list

## Removed
- Remove Vault integration (using Wrangler secrets)
- Remove 6-hour pricing cron (daily sync only)

## Configuration
- Add idx_instance_types_specs_filter composite index
- Add CORS Access-Control-Expose-Headers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 23:50:37 +09:00
parent 9f3d3a245a
commit 3a8dd705e6
47 changed files with 2031 additions and 2459 deletions

View File

@@ -32,15 +32,19 @@ import {
*/
let cachedQueryService: QueryService | null = null;
let cachedCacheService: CacheService | null = null;
let cachedDb: D1Database | null = null;
/**
* Get or create QueryService singleton
* Lazy initialization on first request, then reused for subsequent requests
* Invalidates cache if database binding changes (rolling deploy scenario)
*/
function getQueryService(db: D1Database, env: Env): QueryService {
if (!cachedQueryService) {
// Invalidate cache if db binding changed (rolling deploy scenario)
if (!cachedQueryService || cachedDb !== db) {
cachedQueryService = new QueryService(db, env);
logger.debug('[Instances] QueryService singleton initialized');
cachedDb = db;
logger.debug('[Instances] QueryService singleton initialized/refreshed');
}
return cachedQueryService;
}
@@ -288,6 +292,31 @@ function parseQueryParams(url: URL): {
params.offset = offset;
}
// Range consistency validation
if (params.min_vcpu !== undefined && params.max_vcpu !== undefined) {
if (params.min_vcpu > params.max_vcpu) {
return {
error: {
code: 'INVALID_RANGE',
message: 'min_vcpu cannot be greater than max_vcpu',
parameter: 'min_vcpu',
},
};
}
}
if (params.min_memory_gb !== undefined && params.max_memory_gb !== undefined) {
if (params.min_memory_gb > params.max_memory_gb) {
return {
error: {
code: 'INVALID_RANGE',
message: 'min_memory_gb cannot be greater than max_memory_gb',
parameter: 'min_memory_gb',
},
};
}
}
return { params };
}
@@ -463,8 +492,8 @@ export async function handleInstances(
success: false,
error: {
code: 'QUERY_FAILED',
message: 'Instance query failed',
details: error instanceof Error ? error.message : 'Unknown error',
message: 'Instance query failed. Please try again later.',
request_id: crypto.randomUUID(),
},
},
{ status: HTTP_STATUS.INTERNAL_ERROR }