## 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>
261 lines
6.9 KiB
TypeScript
261 lines
6.9 KiB
TypeScript
/**
|
|
* Anvil Instances Repository
|
|
* Handles CRUD operations for Anvil-branded instance specifications
|
|
*/
|
|
|
|
import { BaseRepository } from './base';
|
|
import { AnvilInstance, AnvilInstanceInput, RepositoryError, ErrorCodes } from '../types';
|
|
|
|
export class AnvilInstancesRepository extends BaseRepository<AnvilInstance> {
|
|
protected tableName = 'anvil_instances';
|
|
protected allowedColumns = [
|
|
'name',
|
|
'display_name',
|
|
'category',
|
|
'vcpus',
|
|
'memory_gb',
|
|
'disk_gb',
|
|
'transfer_tb',
|
|
'network_gbps',
|
|
'gpu_model',
|
|
'gpu_vram_gb',
|
|
'active',
|
|
];
|
|
|
|
/**
|
|
* Find an instance by name (e.g., "anvil-1g-1c")
|
|
*/
|
|
async findByName(name: string): Promise<AnvilInstance | null> {
|
|
try {
|
|
const result = await this.db
|
|
.prepare('SELECT * FROM anvil_instances WHERE name = ?')
|
|
.bind(name)
|
|
.first<AnvilInstance>();
|
|
|
|
return result || null;
|
|
} catch (error) {
|
|
this.logger.error('findByName failed', {
|
|
name,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
throw new RepositoryError(
|
|
`Failed to find Anvil instance by name: ${name}`,
|
|
ErrorCodes.DATABASE_ERROR,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find all instances by category
|
|
*/
|
|
async findByCategory(category: 'vm' | 'gpu' | 'g8' | 'vpu'): Promise<AnvilInstance[]> {
|
|
try {
|
|
const result = await this.db
|
|
.prepare('SELECT * FROM anvil_instances WHERE category = ? ORDER BY name')
|
|
.bind(category)
|
|
.all<AnvilInstance>();
|
|
|
|
return result.results;
|
|
} catch (error) {
|
|
this.logger.error('findByCategory failed', {
|
|
category,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
throw new RepositoryError(
|
|
`Failed to find Anvil instances by category: ${category}`,
|
|
ErrorCodes.DATABASE_ERROR,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all active instances
|
|
*/
|
|
async findActive(category?: 'vm' | 'gpu' | 'g8' | 'vpu'): Promise<AnvilInstance[]> {
|
|
try {
|
|
let query = 'SELECT * FROM anvil_instances WHERE active = 1';
|
|
const params: (string | number | boolean | null)[] = [];
|
|
|
|
if (category) {
|
|
query += ' AND category = ?';
|
|
params.push(category);
|
|
}
|
|
|
|
query += ' ORDER BY name';
|
|
|
|
const result = await this.db
|
|
.prepare(query)
|
|
.bind(...params)
|
|
.all<AnvilInstance>();
|
|
|
|
return result.results;
|
|
} catch (error) {
|
|
this.logger.error('findActive failed', {
|
|
category,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
throw new RepositoryError(
|
|
'Failed to find active Anvil instances',
|
|
ErrorCodes.DATABASE_ERROR,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search instances by resource requirements
|
|
*/
|
|
async searchByResources(
|
|
minVcpus?: number,
|
|
minMemoryGb?: number,
|
|
minDiskGb?: number,
|
|
category?: 'vm' | 'gpu' | 'g8' | 'vpu'
|
|
): Promise<AnvilInstance[]> {
|
|
try {
|
|
const conditions: string[] = ['active = 1'];
|
|
const params: (string | number | boolean | null)[] = [];
|
|
|
|
if (minVcpus !== undefined) {
|
|
conditions.push('vcpus >= ?');
|
|
params.push(minVcpus);
|
|
}
|
|
|
|
if (minMemoryGb !== undefined) {
|
|
conditions.push('memory_gb >= ?');
|
|
params.push(minMemoryGb);
|
|
}
|
|
|
|
if (minDiskGb !== undefined) {
|
|
conditions.push('disk_gb >= ?');
|
|
params.push(minDiskGb);
|
|
}
|
|
|
|
if (category) {
|
|
conditions.push('category = ?');
|
|
params.push(category);
|
|
}
|
|
|
|
const query = 'SELECT * FROM anvil_instances WHERE ' + conditions.join(' AND ') + ' ORDER BY memory_gb, vcpus';
|
|
|
|
const result = await this.db
|
|
.prepare(query)
|
|
.bind(...params)
|
|
.all<AnvilInstance>();
|
|
|
|
return result.results;
|
|
} catch (error) {
|
|
this.logger.error('searchByResources failed', {
|
|
minVcpus,
|
|
minMemoryGb,
|
|
minDiskGb,
|
|
category,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
throw new RepositoryError(
|
|
'Failed to search Anvil instances by resources',
|
|
ErrorCodes.DATABASE_ERROR,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update instance active status
|
|
*/
|
|
async updateActive(id: number, active: boolean): Promise<AnvilInstance> {
|
|
try {
|
|
const result = await this.db
|
|
.prepare('UPDATE anvil_instances SET active = ? WHERE id = ? RETURNING *')
|
|
.bind(active ? 1 : 0, id)
|
|
.first<AnvilInstance>();
|
|
|
|
if (!result) {
|
|
throw new RepositoryError(
|
|
`Anvil instance not found: ${id}`,
|
|
ErrorCodes.NOT_FOUND
|
|
);
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
this.logger.error('updateActive failed', {
|
|
id,
|
|
active,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
|
|
if (error instanceof RepositoryError) {
|
|
throw error;
|
|
}
|
|
|
|
throw new RepositoryError(
|
|
`Failed to update Anvil instance active status: ${id}`,
|
|
ErrorCodes.DATABASE_ERROR,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk upsert instances
|
|
* Uses batch operations for efficiency
|
|
*/
|
|
async upsertMany(instances: AnvilInstanceInput[]): Promise<number> {
|
|
if (instances.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
try {
|
|
const statements = instances.map((instance) => {
|
|
return this.db.prepare(
|
|
`INSERT INTO anvil_instances (
|
|
name, display_name, category, vcpus, memory_gb, disk_gb,
|
|
transfer_tb, network_gbps, gpu_model, gpu_vram_gb, active
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(name)
|
|
DO UPDATE SET
|
|
display_name = excluded.display_name,
|
|
category = excluded.category,
|
|
vcpus = excluded.vcpus,
|
|
memory_gb = excluded.memory_gb,
|
|
disk_gb = excluded.disk_gb,
|
|
transfer_tb = excluded.transfer_tb,
|
|
network_gbps = excluded.network_gbps,
|
|
gpu_model = excluded.gpu_model,
|
|
gpu_vram_gb = excluded.gpu_vram_gb,
|
|
active = excluded.active`
|
|
).bind(
|
|
instance.name,
|
|
instance.display_name,
|
|
instance.category,
|
|
instance.vcpus,
|
|
instance.memory_gb,
|
|
instance.disk_gb,
|
|
instance.transfer_tb,
|
|
instance.network_gbps,
|
|
instance.gpu_model,
|
|
instance.gpu_vram_gb,
|
|
instance.active
|
|
);
|
|
});
|
|
|
|
const successCount = await this.executeBatchCount(statements);
|
|
|
|
this.logger.info('Upserted Anvil instances', { count: successCount });
|
|
return successCount;
|
|
} catch (error) {
|
|
this.logger.error('upsertMany failed', {
|
|
count: instances.length,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
throw new RepositoryError(
|
|
'Failed to upsert Anvil instances',
|
|
ErrorCodes.TRANSACTION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
}
|