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

@@ -80,6 +80,65 @@
* Post-sync invalidation:
* - After sync operation, call cache.delete(key) for all relevant keys
* - Verify next request fetches fresh data from database
*
* Test 9: clearAll Method
* -----------------------
* 1. Set multiple cache entries:
* - await cache.set('key1', { data: 'test1' }, 300)
* - await cache.set('key2', { data: 'test2' }, 300)
* - await cache.set('key3', { data: 'test3' }, 300)
* 2. Call clearAll: const count = await cache.clearAll()
* 3. Expected: count === 0 (enumeration not supported)
* 4. Expected: Log message about TTL-based expiration
* 5. Note: Individual entries will expire based on TTL
*
* Test 10: clearAll with Prefix
* ------------------------------
* 1. Set entries with different prefixes:
* - await cache.set('https://cache.internal/instances?foo=bar', data1)
* - await cache.set('https://cache.internal/pricing?foo=bar', data2)
* 2. Call with prefix: await cache.clearAll('https://cache.internal/instances')
* 3. Expected: count === 0, log shows prefix parameter
* 4. Note: Prefix is logged but enumeration not supported by Cache API
*
* Test 11: clearByEndpoint Method
* --------------------------------
* 1. Set endpoint cache: await cache.set('https://cache.internal/instances', data, 300)
* 2. Clear by endpoint: const deleted = await cache.clearByEndpoint('instances')
* 3. Expected: deleted === true if cache entry existed
* 4. Get data: const result = await cache.get('https://cache.internal/instances')
* 5. Expected: result === null (entry deleted)
* 6. Note: Only exact matches deleted, parameterized queries remain cached
*
* Test 12: clearByEndpoint with Non-existent Endpoint
* ----------------------------------------------------
* 1. Clear non-existent: const deleted = await cache.clearByEndpoint('non-existent')
* 2. Expected: deleted === false
* 3. Expected: Log message about non-existent endpoint
*
* Test 13: Cache Invalidation Strategy
* -------------------------------------
* 1. Set parameterized cache entries:
* - cache.generateKey({ provider: 'linode', region: 'us-east' })
* - cache.generateKey({ provider: 'linode', region: 'eu-west' })
* - cache.generateKey({ provider: 'vultr', region: 'us-east' })
* 2. After schema change or full sync, call clearAll()
* 3. Verify entries expire based on TTL
* 4. For production: Consider using KV-backed cache index for enumeration
*
* Test 14: Error Handling in clearAll
* ------------------------------------
* 1. Mock cache.delete to throw error
* 2. Call clearAll: await cache.clearAll()
* 3. Expected: Error is logged and re-thrown
* 4. Verify error message includes context
*
* Test 15: Error Handling in clearByEndpoint
* -------------------------------------------
* 1. Mock cache.delete to throw error
* 2. Call clearByEndpoint: await cache.clearByEndpoint('instances')
* 3. Expected: Returns false, error logged
* 4. Application continues without crashing
*/
import { CacheService } from './cache';
@@ -146,4 +205,64 @@ async function exampleCacheInvalidationAfterSync(
console.log('[Sync] Cache invalidation complete');
}
export { exampleInstanceEndpointWithCache, exampleCacheInvalidationAfterSync };
/**
* Example: Using clearAll after schema changes
*/
async function exampleClearAllAfterSchemaChange(): Promise<void> {
const cache = new CacheService();
// After major schema changes or data migrations
console.log('[Migration] Clearing all cache entries');
const count = await cache.clearAll();
console.log(`[Migration] Cache clear requested. Entries will expire based on TTL.`);
console.log('[Migration] Consider using KV-backed cache index for enumeration in production.');
}
/**
* Example: Using clearByEndpoint for targeted invalidation
*/
async function exampleClearByEndpointAfterUpdate(endpoint: string): Promise<void> {
const cache = new CacheService();
// Clear cache for specific endpoint after data update
console.log(`[Update] Clearing cache for endpoint: ${endpoint}`);
const deleted = await cache.clearByEndpoint(endpoint);
if (deleted) {
console.log(`[Update] Successfully cleared ${endpoint} cache`);
} else {
console.log(`[Update] No cache entry found for ${endpoint}`);
}
}
/**
* Example: Force cache refresh strategy
*/
async function exampleForceCacheRefresh(
endpoint: string,
fetchFunction: () => Promise<unknown>
): Promise<void> {
const cache = new CacheService();
// Strategy 1: Clear specific endpoint
await cache.clearByEndpoint(endpoint);
// Strategy 2: Clear with prefix (logged but not enumerated)
await cache.clearAll(`https://cache.internal/${endpoint}`);
// Strategy 3: Fetch fresh data and update cache
const freshData = await fetchFunction();
const cacheKey = `https://cache.internal/${endpoint}`;
await cache.set(cacheKey, freshData, 3600);
console.log(`[Refresh] Cache refreshed for ${endpoint}`);
}
export {
exampleInstanceEndpointWithCache,
exampleCacheInvalidationAfterSync,
exampleClearAllAfterSchemaChange,
exampleClearByEndpointAfterUpdate,
exampleForceCacheRefresh
};