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

@@ -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';
}
}