## 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>
129 lines
3.4 KiB
TypeScript
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.
|
|
*/
|