Files
cloud-server/src/connectors/base.ts
kappa 3a8dd705e6 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>
2026-01-25 23:50:37 +09:00

129 lines
3.4 KiB
TypeScript

import type { RegionInput, InstanceTypeInput } from '../types';
import { logger } from '../utils/logger';
/**
* Raw region data from provider API (before normalization)
* Structure varies by provider
*/
export interface RawRegion {
[key: string]: unknown;
}
/**
* Raw instance type data from provider API (before normalization)
* Structure varies by provider
*/
export interface RawInstanceType {
[key: string]: unknown;
}
/**
* Custom error class for connector operations
*
* @example
* throw new ConnectorError('linode', 'fetchRegions', 500, 'API rate limit exceeded');
*/
export class ConnectorError extends Error {
constructor(
public provider: string,
public operation: string,
public statusCode: number | undefined,
message: string
) {
super(message);
this.name = 'ConnectorError';
}
}
/**
* RateLimiter - Token Bucket algorithm implementation
*
* Controls API request rate to prevent hitting provider rate limits.
* Tokens are consumed for each request and refilled at a fixed rate.
*
* @example
* const limiter = new RateLimiter(10, 2); // 10 tokens, refill 2 per second
* await limiter.waitForToken(); // Wait until token is available
* // Make API call
*/
export class RateLimiter {
private tokens: number;
private lastRefillTime: number;
/**
* Create a new rate limiter
*
* @param maxTokens - Maximum number of tokens in the bucket
* @param refillRate - Number of tokens to refill per second
*/
constructor(
private readonly maxTokens: number,
private readonly refillRate: number
) {
this.tokens = maxTokens;
this.lastRefillTime = Date.now();
logger.debug('[RateLimiter] Initialized', { maxTokens, refillRate });
}
/**
* Wait until a token is available, then consume it
* Automatically refills tokens based on elapsed time
*
* @returns Promise that resolves when a token is available
*/
async waitForToken(): Promise<void> {
this.refillTokens();
// If no tokens available, wait until next refill
while (this.tokens < 1) {
const timeUntilNextToken = (1 / this.refillRate) * 1000; // ms per token
await this.sleep(timeUntilNextToken);
this.refillTokens();
}
// Consume one token
this.tokens -= 1;
logger.debug('[RateLimiter] Token consumed', { remaining: this.tokens });
}
/**
* Get the current number of available tokens
*
* @returns Number of available tokens (may include fractional tokens)
*/
getAvailableTokens(): number {
this.refillTokens();
return this.tokens;
}
/**
* Refill tokens based on elapsed time
* Tokens are added proportionally to the time elapsed since last refill
*/
private refillTokens(): void {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefillTime) / 1000;
const tokensToAdd = elapsedSeconds * this.refillRate;
// Add tokens, capped at maxTokens
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefillTime = now;
}
/**
* Sleep for specified milliseconds
*
* @param ms - Milliseconds to sleep
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
/**
* Note: CloudConnector base class has been deprecated.
* Each connector now uses Env directly for credentials instead of Vault.
* See LinodeConnector, VultrConnector, AWSConnector for current implementation patterns.
*/