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:
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user