## 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>
9.8 KiB
/**
- Manual Test Instructions for CacheService
- Since Cloudflare Workers Cache API is only available in the Workers runtime,
- these tests must be run in a Cloudflare Workers environment or using Miniflare.
- Test 1: Basic Cache Operations
-
-
- Deploy to Cloudflare Workers development environment
-
- Initialize cache: const cache = new CacheService(300);
-
- Set data: await cache.set('test-key', { foo: 'bar' }, 60);
-
- Get data: const result = await cache.get('test-key');
-
- Expected: result.data.foo === 'bar', cache_age_seconds ≈ 0
- Test 2: Cache Key Generation
-
-
- Generate key: const key = cache.generateKey({ provider: 'linode', region: 'us-east' });
-
- Expected: key === 'https://cache.internal/instances?provider=linode®ion=us-east'
-
- Verify sorting: cache.generateKey({ z: 1, a: 2 }) should have 'a' before 'z'
- Test 3: Cache Miss
-
-
- Request non-existent key: const result = await cache.get('non-existent');
-
- Expected: result === null
- Test 4: Cache Expiration
-
-
- Set with short TTL: await cache.set('expire-test', { data: 'test' }, 2);
-
- Immediate get: await cache.get('expire-test') → should return data
-
- Wait 3 seconds
-
- Get again: await cache.get('expire-test') → should return null (expired)
- Test 5: Cache Age Tracking
-
-
- Set data: await cache.set('age-test', { data: 'test' }, 300);
-
- Wait 5 seconds
-
- Get data: const result = await cache.get('age-test');
-
- Expected: result.cache_age_seconds ≈ 5
- Test 6: Cache Deletion
-
-
- Set data: await cache.set('delete-test', { data: 'test' }, 300);
-
- Delete: const deleted = await cache.delete('delete-test');
-
- Expected: deleted === true
-
- Get data: const result = await cache.get('delete-test');
-
- Expected: result === null
- Test 7: Error Handling (Graceful Degradation)
-
-
- Test with invalid cache response (manual mock required)
-
- Expected: No errors thrown, graceful null return
-
- Verify logs show error message
- Test 8: Integration with Instance API
-
-
- Create cache instance in instance endpoint handler
-
- Generate key from query params: cache.generateKey(query)
-
- Check cache: const cached = await cache.get<InstanceData[]>(key);
-
- If cache hit: return cached.data with cache metadata
-
- If cache miss: fetch from database, cache result, return data
-
- Verify cache hit on second request
- Performance Validation:
-
-
- Measure database query time (first request)
-
- Measure cache hit time (second request)
-
- Expected: Cache hit 10-50x faster than database query
-
- Verify cache age increases on subsequent requests
- TTL Strategy Validation:
-
- Filtered queries (5 min TTL):
-
- cache.set(key, data, 300)
-
- Verify expires after 5 minutes
- Full dataset (1 hour TTL):
-
- cache.set(key, data, 3600)
-
- Verify expires after 1 hour
- 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
-
-
- 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)
-
- Call clearAll: const count = await cache.clearAll()
-
- Expected: count === 0 (enumeration not supported)
-
- Expected: Log message about TTL-based expiration
-
- Note: Individual entries will expire based on TTL
- Test 10: clearAll with Prefix
-
-
- 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)
-
- Call with prefix: await cache.clearAll('https://cache.internal/instances')
-
- Expected: count === 0, log shows prefix parameter
-
- Note: Prefix is logged but enumeration not supported by Cache API
- Test 11: clearByEndpoint Method
-
-
- Set endpoint cache: await cache.set('https://cache.internal/instances', data, 300)
-
- Clear by endpoint: const deleted = await cache.clearByEndpoint('instances')
-
- Expected: deleted === true if cache entry existed
-
- Get data: const result = await cache.get('https://cache.internal/instances')
-
- Expected: result === null (entry deleted)
-
- Note: Only exact matches deleted, parameterized queries remain cached
- Test 12: clearByEndpoint with Non-existent Endpoint
-
-
- Clear non-existent: const deleted = await cache.clearByEndpoint('non-existent')
-
- Expected: deleted === false
-
- Expected: Log message about non-existent endpoint
- Test 13: Cache Invalidation Strategy
-
-
- 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' })
-
- After schema change or full sync, call clearAll()
-
- Verify entries expire based on TTL
-
- For production: Consider using KV-backed cache index for enumeration
- Test 14: Error Handling in clearAll
-
-
- Mock cache.delete to throw error
-
- Call clearAll: await cache.clearAll()
-
- Expected: Error is logged and re-thrown
-
- Verify error message includes context
- Test 15: Error Handling in clearByEndpoint
-
-
- Mock cache.delete to throw error
-
- Call clearByEndpoint: await cache.clearByEndpoint('instances')
-
- Expected: Returns false, error logged
-
- Application continues without crashing */
import { CacheService } from './cache'; import type { InstanceData } from '../types';
/**
- Example: Using CacheService in API endpoint */ async function exampleInstanceEndpointWithCache( queryParams: Record<string, unknown>, fetchFromDatabase: () => Promise<InstanceData[]> ): Promise<{ data: InstanceData[]; cached?: boolean; cache_age?: number }> { const cache = new CacheService(300); // 5 minutes default TTL
// Generate cache key from query parameters const cacheKey = cache.generateKey(queryParams);
// Try to get from cache const cached = await cache.get<InstanceData[]>(cacheKey);
if (cached) {
console.log([API] Cache hit (age: ${cached.cache_age_seconds}s));
return {
data: cached.data,
cached: true,
cache_age: cached.cache_age_seconds,
};
}
// Cache miss - fetch from database console.log('[API] Cache miss - fetching from database'); const data = await fetchFromDatabase();
// Determine TTL based on query complexity const hasFilters = Object.keys(queryParams).length > 0; const ttl = hasFilters ? 300 : 3600; // 5 min for filtered, 1 hour for full
// Store in cache await cache.set(cacheKey, data, ttl);
return { data, cached: false, }; }
/**
- Example: Cache invalidation after sync */ async function exampleCacheInvalidationAfterSync( syncedProviders: string[] ): Promise { const cache = new CacheService();
// Invalidate all instance caches for synced providers
for (const provider of syncedProviders) {
// Note: Since Cloudflare Workers Cache API doesn't support pattern matching,
// you need to maintain a list of active cache keys or use KV for indexing
const key = cache.generateKey({ provider });
await cache.delete(key);
console.log([Sync] Invalidated cache for provider: ${provider});
}
console.log('[Sync] Cache invalidation complete'); }
/**
- Example: Using clearAll after schema changes */ async function exampleClearAllAfterSchemaChange(): Promise { 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 { 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 ): Promise { 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 };