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:
@@ -1,7 +1,7 @@
|
||||
import type { RegionInput, InstanceTypeInput, InstanceFamily } from '../types';
|
||||
import { VaultClient, VaultError } from './vault';
|
||||
import type { Env, RegionInput, InstanceTypeInput, InstanceFamily } from '../types';
|
||||
import { RateLimiter } from './base';
|
||||
import { TIMEOUTS, HTTP_STATUS } from '../constants';
|
||||
import { createLogger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* AWS connector error class
|
||||
@@ -56,10 +56,10 @@ interface EC2ShopResponse {
|
||||
* - Rate limiting: 20 requests/second
|
||||
* - Hardcoded region list (relatively static)
|
||||
* - Comprehensive error handling
|
||||
* - Optional AWS credentials from environment for future API integration
|
||||
*
|
||||
* @example
|
||||
* const vault = new VaultClient(vaultUrl, vaultToken);
|
||||
* const connector = new AWSConnector(vault);
|
||||
* const connector = new AWSConnector(env);
|
||||
* await connector.initialize();
|
||||
* const regions = await connector.fetchRegions();
|
||||
*/
|
||||
@@ -68,6 +68,7 @@ export class AWSConnector {
|
||||
private readonly instanceDataUrl = 'https://ec2.shop/?json';
|
||||
private readonly rateLimiter: RateLimiter;
|
||||
private readonly requestTimeout = TIMEOUTS.AWS_REQUEST;
|
||||
private readonly logger = createLogger('[AWSConnector]');
|
||||
|
||||
/**
|
||||
* AWS regions list (relatively static data)
|
||||
@@ -105,42 +106,27 @@ export class AWSConnector {
|
||||
{ code: 'il-central-1', name: 'Israel (Tel Aviv)' },
|
||||
];
|
||||
|
||||
constructor(private vaultClient: VaultClient) {
|
||||
constructor(private env: Env) {
|
||||
// Rate limit: 20 requests/second per region
|
||||
// Use 10 tokens with 10/second refill to be conservative
|
||||
this.rateLimiter = new RateLimiter(20, 10);
|
||||
console.log('[AWSConnector] Initialized');
|
||||
this.logger.info('Initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize connector by fetching credentials from Vault
|
||||
* Note: Currently not required for public API access,
|
||||
* Initialize connector by loading credentials from environment
|
||||
* Note: Currently not required for public API access (ec2.shop),
|
||||
* but included for future AWS API integration
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
console.log('[AWSConnector] Fetching credentials from Vault');
|
||||
this.logger.info('Loading credentials from environment');
|
||||
|
||||
try {
|
||||
const credentials = await this.vaultClient.getCredentials(this.provider);
|
||||
|
||||
// AWS uses different credential keys
|
||||
const awsCreds = credentials as unknown as {
|
||||
aws_access_key_id?: string;
|
||||
aws_secret_access_key?: string;
|
||||
};
|
||||
|
||||
// Credentials loaded for future AWS API direct access
|
||||
console.log('[AWSConnector] Credentials loaded successfully', {
|
||||
hasAccessKey: !!awsCreds.aws_access_key_id,
|
||||
hasSecretKey: !!awsCreds.aws_secret_access_key,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof VaultError) {
|
||||
console.warn('[AWSConnector] Vault credentials not available, using public API only');
|
||||
// Not critical for public API access
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
// AWS credentials are optional since we use public ec2.shop API
|
||||
// They would be required for direct AWS API access
|
||||
if (this.env.AWS_ACCESS_KEY_ID && this.env.AWS_SECRET_ACCESS_KEY) {
|
||||
this.logger.info('AWS credentials available for future API access');
|
||||
} else {
|
||||
this.logger.info('Using public ec2.shop API (no AWS credentials required)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +137,7 @@ export class AWSConnector {
|
||||
* @returns Array of AWS regions
|
||||
*/
|
||||
async fetchRegions(): Promise<AWSRegion[]> {
|
||||
console.log('[AWSConnector] Fetching regions', { count: this.awsRegions.length });
|
||||
this.logger.info('Fetching regions', { count: this.awsRegions.length });
|
||||
return this.awsRegions;
|
||||
}
|
||||
|
||||
@@ -162,7 +148,7 @@ export class AWSConnector {
|
||||
* @throws AWSError on API failures
|
||||
*/
|
||||
async fetchInstanceTypes(): Promise<AWSInstanceType[]> {
|
||||
console.log('[AWSConnector] Fetching instance types from ec2.shop');
|
||||
this.logger.info('Fetching instance types from ec2.shop');
|
||||
|
||||
await this.rateLimiter.waitForToken();
|
||||
|
||||
@@ -190,13 +176,13 @@ export class AWSConnector {
|
||||
const data = await response.json() as EC2ShopResponse;
|
||||
|
||||
const instances = data.Prices || [];
|
||||
console.log('[AWSConnector] Instance types fetched', { count: instances.length });
|
||||
this.logger.info('Instance types fetched', { count: instances.length });
|
||||
return instances;
|
||||
|
||||
} catch (error) {
|
||||
// Handle timeout
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.error('[AWSConnector] Request timeout', { timeout: this.requestTimeout });
|
||||
this.logger.error('Request timeout', { timeout: this.requestTimeout });
|
||||
throw new AWSError(
|
||||
`Request to ec2.shop API timed out after ${this.requestTimeout}ms`,
|
||||
504
|
||||
@@ -209,7 +195,7 @@ export class AWSConnector {
|
||||
}
|
||||
|
||||
// Handle unexpected errors
|
||||
console.error('[AWSConnector] Unexpected error', { error });
|
||||
this.logger.error('Unexpected error', { error });
|
||||
throw new AWSError(
|
||||
`Failed to fetch instance types: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
HTTP_STATUS.INTERNAL_ERROR,
|
||||
@@ -515,7 +501,7 @@ export class AWSConnector {
|
||||
}
|
||||
|
||||
// Default to general for unknown types
|
||||
console.warn('[AWSConnector] Unknown instance family, defaulting to general', { type: instanceType });
|
||||
this.logger.warn('Unknown instance family, defaulting to general', { type: instanceType });
|
||||
return 'general';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user