Files
cloud-server/src/repositories/anvil-instances.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

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
);
}
}
}