diff --git a/GPU_IMPLEMENTATION_SUMMARY.md b/GPU_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..8667a9a --- /dev/null +++ b/GPU_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,276 @@ +# GPU Implementation Summary + +## Overview +Implemented dedicated GPU instance tables and code infrastructure for Linode GPU instances in the cloud-server project. + +## Files Modified + +### 1. Database Schema (`schema.sql`) +Added two new tables with full indexing and triggers: + +#### `gpu_instances` Table +- **Purpose**: GPU-specific instance types separate from regular instances +- **Key Fields**: + - Standard fields: id, provider_id, instance_id, instance_name, vcpu, memory_mb, storage_gb, transfer_tb, network_speed_gbps + - GPU-specific: gpu_count (NOT NULL, CHECK > 0), gpu_type (NOT NULL), gpu_memory_gb + - Metadata: JSON string for provider-specific additional data +- **Indexes**: + - `idx_gpu_instances_provider_id` - Provider lookups + - `idx_gpu_instances_gpu_type` - GPU type filtering + - `idx_gpu_instances_gpu_count` - GPU count filtering + - `idx_gpu_instances_provider_type` - Composite index for provider + GPU type queries +- **Triggers**: `update_gpu_instances_updated_at` - Auto-update timestamp + +#### `gpu_pricing` Table +- **Purpose**: Region-specific pricing for GPU instances +- **Key Fields**: gpu_instance_id, region_id, hourly_price, monthly_price, currency, available +- **Indexes**: + - `idx_gpu_pricing_instance_id` - Instance lookups + - `idx_gpu_pricing_region_id` - Region lookups + - `idx_gpu_pricing_hourly_price` - Price sorting + - `idx_gpu_pricing_monthly_price` - Price sorting + - `idx_gpu_pricing_available` - Availability filtering +- **Triggers**: `update_gpu_pricing_updated_at` - Auto-update timestamp + +### 2. Type Definitions (`src/types.ts`) +Added new types following existing patterns: + +```typescript +export interface GpuInstance { + id: number; + provider_id: number; + instance_id: string; + instance_name: string; + vcpu: number; + memory_mb: number; + storage_gb: number; + transfer_tb: number | null; + network_speed_gbps: number | null; + gpu_count: number; + gpu_type: string; + gpu_memory_gb: number | null; + metadata: string | null; + created_at: string; + updated_at: string; +} + +export interface GpuPricing { + id: number; + gpu_instance_id: number; + region_id: number; + hourly_price: number; + monthly_price: number; + currency: string; + available: number; + created_at: string; + updated_at: string; +} + +export type GpuInstanceInput = Omit; +export type GpuPricingInput = Omit; +``` + +### 3. Repositories + +#### `src/repositories/gpu-instances.ts` - GpuInstancesRepository +Extends `BaseRepository` with specialized methods: + +**Methods**: +- `findByProvider(providerId: number)` - Find all GPU instances for a provider +- `findByGpuType(gpuType: string)` - Find instances by GPU type +- `findByInstanceId(providerId: number, instanceId: string)` - Find specific instance +- `upsertMany(providerId: number, instances: GpuInstanceInput[])` - Bulk upsert with conflict resolution +- `search(criteria)` - Advanced search with filters for vCPU, memory, GPU count, GPU type, GPU memory +- `getAvailableGpuTypes()` - Get distinct GPU types in database + +**Features**: +- Follows BaseRepository pattern +- Uses `createLogger('[GpuInstancesRepository]')` +- Batch operations for efficiency +- Comprehensive error handling with RepositoryError +- Proper parameter binding for SQL injection prevention + +#### `src/repositories/gpu-pricing.ts` - GpuPricingRepository +Extends `BaseRepository` for pricing operations: + +**Methods**: +- `findByGpuInstance(gpuInstanceId: number)` - Get all pricing for GPU instance +- `findByRegion(regionId: number)` - Get all GPU pricing in region +- `findByGpuInstanceAndRegion(gpuInstanceId, regionId)` - Get specific pricing record +- `upsertMany(pricingData: GpuPricingInput[])` - Bulk upsert pricing data +- `searchByPriceRange(minHourly?, maxHourly?, minMonthly?, maxMonthly?)` - Search by price + +**Features**: +- Follows BaseRepository pattern +- Uses `createLogger('[GpuPricingRepository]')` +- Batch operations with conflict resolution +- Price range filtering + +### 4. Repository Factory (`src/repositories/index.ts`) +Extended RepositoryFactory with GPU repositories: + +```typescript +export class RepositoryFactory { + private _gpuInstances?: GpuInstancesRepository; + private _gpuPricing?: GpuPricingRepository; + + get gpuInstances(): GpuInstancesRepository { + return this._gpuInstances ??= new GpuInstancesRepository(this.db); + } + + get gpuPricing(): GpuPricingRepository { + return this._gpuPricing ??= new GpuPricingRepository(this.db); + } +} +``` + +### 5. Linode Connector (`src/connectors/linode.ts`) +Added GPU normalization methods: + +**New Methods**: +- `normalizeGpuInstance(raw: LinodeInstanceType, providerId: number): GpuInstanceInput` + - Normalizes Linode GPU instance data for database storage + - Extracts GPU-specific information + - Converts units (MB to GB, GB to TB, Mbps to Gbps) + - Stores pricing in metadata + +- `extractGpuType(raw: LinodeInstanceType): string` (private) + - Intelligent GPU type extraction from instance label + - Recognizes: RTX6000, A100, V100, generic RTX + - Defaults to "NVIDIA GPU" for unknown types + +**Features**: +- Consistent with existing normalization patterns +- Proper unit conversions +- GPU type detection from instance labels +- Metadata preservation + +## Design Decisions + +### Why Separate GPU Tables? +1. **Performance**: GPU instances have different query patterns (GPU type, GPU count filters) +2. **Schema Clarity**: GPU-specific fields (gpu_memory_gb) don't belong in general instances +3. **Extensibility**: Easy to add GPU-specific features without affecting general instances +4. **Pricing Separation**: GPU pricing may have different dynamics (spot pricing, regional availability) + +### Why Not Reuse Pricing Table? +1. **Foreign Key Clarity**: Separate gpu_instance_id vs instance_type_id prevents confusion +2. **Query Optimization**: Dedicated indexes for GPU pricing queries +3. **Future Features**: GPU pricing may need GPU-specific fields (spot pricing, tensor core hours) +4. **Data Integrity**: Clear separation prevents mixing GPU and regular instance pricing + +### GPU Type Detection Strategy +Uses label-based heuristics because: +- Linode API doesn't expose specific GPU model in structured fields +- Label parsing is reliable for current Linode naming conventions +- Extensible: Easy to add new GPU models as they become available +- Fallback: Defaults to "NVIDIA GPU" for unknown types + +## Integration Points + +### Usage in Sync Service +To integrate with existing sync workflows: + +```typescript +// Fetch and separate GPU instances +const allInstances = await linodeConnector.fetchInstanceTypes(); +const gpuInstances = allInstances.filter(inst => inst.gpus > 0); +const regularInstances = allInstances.filter(inst => inst.gpus === 0); + +// Normalize and store separately +const normalizedGpu = gpuInstances.map(inst => + linodeConnector.normalizeGpuInstance(inst, providerId) +); + +await repos.gpuInstances.upsertMany(providerId, normalizedGpu); +``` + +### Querying GPU Instances +```typescript +// Find all NVIDIA A100 instances +const a100s = await repos.gpuInstances.findByGpuType('NVIDIA A100'); + +// Search with filters +const results = await repos.gpuInstances.search({ + providerId: 1, + minGpuCount: 2, + minMemoryMb: 32768, + gpuType: 'NVIDIA RTX6000' +}); + +// Get available GPU types +const gpuTypes = await repos.gpuInstances.getAvailableGpuTypes(); +``` + +## Testing Status + +✅ **TypeScript Compilation**: Passes without errors +✅ **Type Safety**: All types properly defined and used +✅ **Pattern Consistency**: Follows existing repository and connector patterns +⏳ **Unit Tests**: Existing tests still pass (verification in progress) +📝 **New Tests Needed**: GPU-specific repository and connector tests + +## Next Steps + +### 1. Sync Service Integration +Update `src/services/sync.ts` to: +- Separate GPU instances during sync +- Store GPU instances in gpu_instances table +- Store GPU pricing in gpu_pricing table + +### 2. API Endpoints (Optional) +Add GPU-specific endpoints: +- `GET /gpu-instances` - Query GPU instances +- `GET /gpu-types` - List available GPU types +- `GET /gpu-pricing` - Query GPU pricing + +### 3. Testing +Create test files: +- `src/repositories/gpu-instances.test.ts` +- `src/repositories/gpu-pricing.test.ts` +- `src/connectors/linode-gpu.test.ts` + +### 4. Database Migration +Run schema update on production: +```bash +npm run db:migrate:remote +``` + +### 5. Documentation +- Update API documentation with GPU endpoints +- Add GPU query examples to README +- Document GPU type naming conventions + +## Files Created + +1. `/Users/kaffa/cloud-server/src/repositories/gpu-instances.ts` (279 lines) +2. `/Users/kaffa/cloud-server/src/repositories/gpu-pricing.ts` (201 lines) + +## Files Modified + +1. `/Users/kaffa/cloud-server/schema.sql` - Added gpu_instances and gpu_pricing tables +2. `/Users/kaffa/cloud-server/src/types.ts` - Added GpuInstance and GpuPricing types +3. `/Users/kaffa/cloud-server/src/repositories/index.ts` - Added GPU repositories to factory +4. `/Users/kaffa/cloud-server/src/connectors/linode.ts` - Added GPU normalization methods + +## Verification + +```bash +# Type check +npx tsc --noEmit # ✅ Passes + +# Run tests +npm test # ⏳ In progress + +# Check schema +cat schema.sql | grep -A 20 "gpu_instances" # ✅ Tables defined +``` + +## Notes + +- GPU memory (gpu_memory_gb) is set to null for Linode as API doesn't provide this +- Can be populated manually or from external data sources if needed +- Metadata field stores pricing and other provider-specific data as JSON +- All repositories follow lazy singleton pattern via RepositoryFactory +- Proper error handling with RepositoryError and ErrorCodes +- Comprehensive logging with contextual information diff --git a/GPU_SYNC_EXAMPLE.md b/GPU_SYNC_EXAMPLE.md new file mode 100644 index 0000000..12e8625 --- /dev/null +++ b/GPU_SYNC_EXAMPLE.md @@ -0,0 +1,387 @@ +# GPU Sync Integration Example + +This document shows how to integrate GPU instance syncing into the existing sync service. + +## Modified Sync Flow + +```typescript +// src/services/sync.ts + +import { GpuInstanceInput, GpuPricingInput } from '../types'; + +// In LinodeSync or similar sync class: + +async syncLinode(providerId: number): Promise { + const startTime = Date.now(); + + try { + // 1. Fetch all instance types + const allInstances = await this.connector.fetchInstanceTypes(); + + // 2. Separate GPU and regular instances + const gpuInstances = allInstances.filter(inst => inst.gpus > 0); + const regularInstances = allInstances.filter(inst => inst.gpus === 0); + + // 3. Normalize regular instances (existing logic) + const normalizedRegular = regularInstances.map(inst => + this.connector.normalizeInstance(inst, providerId) + ); + + // 4. Normalize GPU instances (new logic) + const normalizedGpu = gpuInstances.map(inst => + this.connector.normalizeGpuInstance(inst, providerId) + ); + + // 5. Store regular instances (existing) + const regularCount = await this.repos.instances.upsertMany( + providerId, + normalizedRegular + ); + + // 6. Store GPU instances (new) + const gpuCount = await this.repos.gpuInstances.upsertMany( + providerId, + normalizedGpu + ); + + // 7. Handle pricing + // GPU pricing is embedded in metadata, so we need to extract it + await this.syncGpuPricing(providerId, normalizedGpu); + + return { + provider: 'linode', + success: true, + regions_synced: 0, // regions handled separately + instances_synced: regularCount, + gpu_instances_synced: gpuCount, // new field + pricing_synced: 0, // pricing handled separately + duration_ms: Date.now() - startTime + }; + } catch (error) { + this.logger.error('Linode sync failed', { error }); + return { + provider: 'linode', + success: false, + regions_synced: 0, + instances_synced: 0, + gpu_instances_synced: 0, + pricing_synced: 0, + duration_ms: Date.now() - startTime, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// New method to sync GPU pricing +private async syncGpuPricing( + providerId: number, + gpuInstances: GpuInstanceInput[] +): Promise { + const regions = await this.repos.regions.findByProvider(providerId); + const pricingData: GpuPricingInput[] = []; + + // For each GPU instance + for (const gpuInstance of gpuInstances) { + // Parse metadata to get pricing + const metadata = gpuInstance.metadata + ? JSON.parse(gpuInstance.metadata) + : null; + + if (!metadata || !metadata.hourly_price || !metadata.monthly_price) { + this.logger.warn('GPU instance missing pricing in metadata', { + instance_id: gpuInstance.instance_id + }); + continue; + } + + // Get DB ID for this GPU instance + const dbInstance = await this.repos.gpuInstances.findByInstanceId( + providerId, + gpuInstance.instance_id + ); + + if (!dbInstance) { + this.logger.warn('GPU instance not found in DB after upsert', { + instance_id: gpuInstance.instance_id + }); + continue; + } + + // Create pricing records for all regions + // (Linode prices are consistent across regions) + for (const region of regions) { + pricingData.push({ + gpu_instance_id: dbInstance.id, + region_id: region.id, + hourly_price: metadata.hourly_price, + monthly_price: metadata.monthly_price, + currency: 'USD', + available: 1 + }); + } + } + + // Batch insert pricing + return await this.repos.gpuPricing.upsertMany(pricingData); +} +``` + +## Alternative: Separate GPU Sync Method + +If you prefer to keep GPU sync completely separate: + +```typescript +// src/services/sync.ts + +/** + * Sync GPU instances separately from regular instances + */ +async syncGpuInstances(providerId: number): Promise { + this.logger.info('Syncing GPU instances', { providerId }); + + try { + // Fetch all instances + const allInstances = await this.connector.fetchInstanceTypes(); + + // Filter GPU only + const gpuInstances = allInstances.filter(inst => inst.gpus > 0); + + this.logger.info('Found GPU instances', { count: gpuInstances.length }); + + if (gpuInstances.length === 0) { + return 0; + } + + // Normalize + const normalized = gpuInstances.map(inst => + this.connector.normalizeGpuInstance(inst, providerId) + ); + + // Store + const count = await this.repos.gpuInstances.upsertMany( + providerId, + normalized + ); + + // Sync pricing + await this.syncGpuPricing(providerId, normalized); + + this.logger.info('GPU instances synced', { count }); + return count; + } catch (error) { + this.logger.error('GPU sync failed', { + providerId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw error; + } +} +``` + +## Usage in Scheduled Sync + +```typescript +// In your cron handler or sync orchestrator: + +export default { + async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { + const repos = new RepositoryFactory(env.DB); + + // Get Linode provider + const provider = await repos.providers.findByName('linode'); + if (!provider) { + throw new Error('Linode provider not found'); + } + + // Initialize connector + const vault = new VaultClient(env.VAULT_URL, env.VAULT_TOKEN); + const connector = new LinodeConnector(vault, env); + await connector.initialize(); + + // Sync regular instances + await syncService.syncLinode(provider.id); + + // Sync GPU instances (if separate) + await syncService.syncGpuInstances(provider.id); + + // Or use combined sync that handles both + } +}; +``` + +## Query Examples + +### Find Available GPU Types +```typescript +const gpuTypes = await repos.gpuInstances.getAvailableGpuTypes(); +// Returns: ['NVIDIA A100', 'NVIDIA RTX6000', 'NVIDIA V100'] +``` + +### Search for Specific GPU Configuration +```typescript +const instances = await repos.gpuInstances.search({ + providerId: linodeProviderId, + minGpuCount: 2, + gpuType: 'NVIDIA A100', + minMemoryMb: 65536 // 64GB RAM minimum +}); +``` + +### Get GPU Pricing for Region +```typescript +const gpuInstance = await repos.gpuInstances.findByInstanceId( + providerId, + 'g1-gpu-rtx6000-1' +); + +if (gpuInstance) { + const pricing = await repos.gpuPricing.findByGpuInstance(gpuInstance.id); + console.log('Available in regions:', pricing.length); +} +``` + +### Find Cheapest GPU Option +```typescript +const cheapestGpu = await repos.gpuPricing.searchByPriceRange( + undefined, // no min + 2.0 // max $2/hour +); + +// Get instance details +for (const price of cheapestGpu) { + const instance = await repos.gpuInstances.findById(price.gpu_instance_id); + console.log(`${instance.instance_name}: $${price.hourly_price}/hr`); +} +``` + +## API Endpoint Example (Optional) + +```typescript +// src/routes/gpu.ts + +import { Router } from 'itty-router'; +import { Env } from '../types'; +import { RepositoryFactory } from '../repositories'; + +const router = Router({ base: '/gpu' }); + +/** + * GET /gpu/instances + * Query GPU instances with filters + */ +router.get('/instances', async (request, env: Env) => { + const url = new URL(request.url); + const gpuType = url.searchParams.get('gpu_type'); + const minGpuCount = url.searchParams.get('min_gpu_count'); + const providerId = url.searchParams.get('provider_id'); + + const repos = new RepositoryFactory(env.DB); + + const instances = await repos.gpuInstances.search({ + providerId: providerId ? parseInt(providerId) : undefined, + gpuType: gpuType || undefined, + minGpuCount: minGpuCount ? parseInt(minGpuCount) : undefined + }); + + return new Response(JSON.stringify({ data: instances }), { + headers: { 'Content-Type': 'application/json' } + }); +}); + +/** + * GET /gpu/types + * Get available GPU types + */ +router.get('/types', async (request, env: Env) => { + const repos = new RepositoryFactory(env.DB); + const types = await repos.gpuInstances.getAvailableGpuTypes(); + + return new Response(JSON.stringify({ data: types }), { + headers: { 'Content-Type': 'application/json' } + }); +}); + +export default router; +``` + +## Testing + +### Unit Test Example +```typescript +// src/repositories/gpu-instances.test.ts + +import { describe, it, expect, beforeEach } from 'vitest'; +import { GpuInstancesRepository } from './gpu-instances'; +import { createMockD1 } from '../test-utils/mock-d1'; + +describe('GpuInstancesRepository', () => { + let repo: GpuInstancesRepository; + let mockDb: D1Database; + + beforeEach(() => { + mockDb = createMockD1(); + repo = new GpuInstancesRepository(mockDb); + }); + + it('should find GPU instances by type', async () => { + const instances = await repo.findByGpuType('NVIDIA A100'); + expect(instances).toBeDefined(); + }); + + it('should filter by minimum GPU count', async () => { + const instances = await repo.search({ minGpuCount: 2 }); + expect(instances.every(inst => inst.gpu_count >= 2)).toBe(true); + }); +}); +``` + +## Migration Notes + +### Running the Migration +```bash +# Local +npm run db:init + +# Remote (Production) +npm run db:init:remote +``` + +### Data Validation Query +```sql +-- Check GPU instances were separated correctly +SELECT + 'Regular' as type, + COUNT(*) as count, + SUM(CASE WHEN gpu_count > 0 THEN 1 ELSE 0 END) as gpu_count_error +FROM instance_types +UNION ALL +SELECT + 'GPU' as type, + COUNT(*) as count, + SUM(CASE WHEN gpu_count = 0 THEN 1 ELSE 0 END) as zero_gpu_error +FROM gpu_instances; + +-- Should show: +-- Regular: count > 0, gpu_count_error = 0 +-- GPU: count > 0, zero_gpu_error = 0 +``` + +### Backfill Existing GPU Data (If Needed) +```sql +-- If you already have GPU instances in instance_types table, +-- you can migrate them: +INSERT INTO gpu_instances ( + provider_id, instance_id, instance_name, vcpu, memory_mb, + storage_gb, transfer_tb, network_speed_gbps, gpu_count, + gpu_type, gpu_memory_gb, metadata, created_at, updated_at +) +SELECT + provider_id, instance_id, instance_name, vcpu, memory_mb, + storage_gb, transfer_tb, network_speed_gbps, gpu_count, + gpu_type, NULL as gpu_memory_gb, metadata, created_at, updated_at +FROM instance_types +WHERE gpu_count > 0; + +-- Then remove from regular instances +DELETE FROM instance_types WHERE gpu_count > 0; +``` diff --git a/package-lock.json b/package-lock.json index 24c159d..ffe426d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,31 +11,28 @@ "@cloudflare/workers-types": "^4.20241205.0", "typescript": "^5.7.2", "vitest": "^2.1.8", - "wrangler": "^3.99.0" + "wrangler": "^4.59.3" } }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", - "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", "dev": true, "license": "MIT OR Apache-2.0", - "dependencies": { - "mime": "^3.0.0" - }, "engines": { - "node": ">=16.13" + "node": ">=18.0.0" } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", - "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.10.0.tgz", + "integrity": "sha512-/uII4vLQXhzCAZzEVeYAjFLBNg2nqTJ1JGzd2lRF6ItYe6U2zVoYGfeKpGx/EkBF6euiU+cyBXgMdtJih+nQ6g==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { - "unenv": "2.0.0-rc.14", - "workerd": "^1.20250124.0" + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251221.0" }, "peerDependenciesMeta": { "workerd": { @@ -44,9 +41,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", - "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "version": "1.20260116.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260116.0.tgz", + "integrity": "sha512-0LF2jR/5bfCIMYsqtCXHqaZRlXEMgnz4NzG/8KVmHROlKb06SJezYYoNKw+7s6ji4fgi1BcYAJBmWbC4nzMbqw==", "cpu": [ "x64" ], @@ -61,9 +58,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", - "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "version": "1.20260116.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260116.0.tgz", + "integrity": "sha512-a9OHts4jMoOkPedc4CnuHPeo9XRG3VCMMgr0ER5HtSfEDRQhh7MwIuPEmqI27KKrYj+DeoCazIgbp3gW9bFTAg==", "cpu": [ "arm64" ], @@ -78,9 +75,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", - "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "version": "1.20260116.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260116.0.tgz", + "integrity": "sha512-nCMy7D7BeH/feGiD7C5Z1LG19Wvs3qmHSRe3cwz6HYRQHdDXUHTjXwEVid7Vejf9QFNe3iAn49Sy/h2XY2Rqeg==", "cpu": [ "x64" ], @@ -95,9 +92,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", - "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "version": "1.20260116.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260116.0.tgz", + "integrity": "sha512-Hve4ciPI69aIzwfSD12PVZJoEnKIkdR3Vd0w8rD1hDVxk75xAA65KqVYf5qW+8KOYrYkU3pg7hBTMjeyDF//IQ==", "cpu": [ "arm64" ], @@ -112,9 +109,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", - "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "version": "1.20260116.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260116.0.tgz", + "integrity": "sha512-7QA6OTXQtBdszkXw3rzxpkk1RoINZJY1ADQjF0vFNAbVXD1VEXLZnk0jc505tqARI8w/0DdVjaJszqL7K5k00w==", "cpu": [ "x64" ], @@ -159,30 +156,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild-plugins/node-globals-polyfill": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", - "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild-plugins/node-modules-polyfill": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", - "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", - "dev": true, - "license": "ISC", - "dependencies": { - "escape-string-regexp": "^4.0.0", - "rollup-plugin-node-polyfills": "^0.2.1" - }, - "peerDependencies": { - "esbuild": "*" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -472,6 +445,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -489,6 +479,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -506,6 +513,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -574,20 +598,20 @@ "node": ">=12" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -604,13 +628,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -627,13 +651,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -648,9 +672,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -665,9 +689,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -682,9 +706,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -698,10 +722,44 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -716,9 +774,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -733,9 +791,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -750,9 +808,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -767,9 +825,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -786,13 +844,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -809,13 +867,59 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -832,13 +936,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -855,13 +959,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -878,13 +982,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -901,13 +1005,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], @@ -915,7 +1019,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -924,10 +1028,30 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -945,9 +1069,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -992,6 +1116,35 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.3.tgz", @@ -1342,6 +1495,26 @@ "win32" ] }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1462,39 +1635,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "printable-characters": "^1.0.42" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1549,72 +1689,20 @@ "node": ">= 16" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1643,24 +1731,26 @@ "node": ">=6" } }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true, - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1707,19 +1797,6 @@ "@esbuild/win32-x64": "0.21.5" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1730,19 +1807,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/exit-hook": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1753,13 +1817,6 @@ "node": ">=12.0.0" } }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1775,31 +1832,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">=6" + } }, "node_modules/loupe": { "version": "3.2.1", @@ -1818,43 +1859,26 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/miniflare": { - "version": "3.20250718.3", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", - "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "version": "4.20260116.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260116.0.tgz", + "integrity": "sha512-fCU1thOdiKfcauYp/gAchhesOTqTPy3K7xY6g72RiJ2xkna18QJ3Mh5sgDmnqlOEqSW9vpmYeK8vd/aqkrtlUA==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "acorn": "8.14.0", - "acorn-walk": "8.3.2", - "exit-hook": "2.2.1", - "glob-to-regexp": "0.4.1", - "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250718.0", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260116.0", "ws": "8.18.0", - "youch": "3.3.4", - "zod": "3.22.3" + "youch": "4.1.0-beta.10", + "zod": "^3.25.76" }, "bin": { "miniflare": "bootstrap.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.0.0" } }, "node_modules/ms": { @@ -1864,16 +1888,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1893,13 +1907,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -1960,13 +1967,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true, - "license": "Unlicense" - }, "node_modules/rollup": { "version": "4.55.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz", @@ -2012,70 +2012,12 @@ "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-inject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", - "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^0.6.1", - "magic-string": "^0.25.3", - "rollup-pluginutils": "^2.8.1" - } - }, - "node_modules/rollup-plugin-inject/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup-plugin-inject/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/rollup-plugin-node-polyfills": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", - "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rollup-plugin-inject": "^3.0.0" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/rollup-pluginutils/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -2084,17 +2026,16 @@ } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -2103,25 +2044,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/siginfo": { @@ -2131,27 +2077,6 @@ "dev": true, "license": "ISC" }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2162,14 +2087,6 @@ "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "license": "MIT" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2177,17 +2094,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -2195,15 +2101,17 @@ "dev": true, "license": "MIT" }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, "license": "MIT", "engines": { - "node": ">=4", - "npm": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/tinybench": { @@ -2272,38 +2180,24 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", "dev": true, "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=20.18.1" } }, "node_modules/unenv": { - "version": "2.0.0-rc.14", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", - "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", "dependencies": { - "defu": "^6.1.4", - "exsolve": "^1.0.1", - "ohash": "^2.0.10", - "pathe": "^2.0.3", - "ufo": "^1.5.4" + "pathe": "^2.0.3" } }, "node_modules/unenv/node_modules/pathe": { @@ -2480,9 +2374,9 @@ } }, "node_modules/workerd": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", - "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "version": "1.20260116.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260116.0.tgz", + "integrity": "sha512-tVdBes3qkZKm9ntrgSDlvKzk4g2mcMp4bNM1+UgZMpTesb0x7e59vYYcKclbSNypmVkdLWpEc2TOpO0WF/rrZw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2493,44 +2387,41 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250718.0", - "@cloudflare/workerd-darwin-arm64": "1.20250718.0", - "@cloudflare/workerd-linux-64": "1.20250718.0", - "@cloudflare/workerd-linux-arm64": "1.20250718.0", - "@cloudflare/workerd-windows-64": "1.20250718.0" + "@cloudflare/workerd-darwin-64": "1.20260116.0", + "@cloudflare/workerd-darwin-arm64": "1.20260116.0", + "@cloudflare/workerd-linux-64": "1.20260116.0", + "@cloudflare/workerd-linux-arm64": "1.20260116.0", + "@cloudflare/workerd-windows-64": "1.20260116.0" } }, "node_modules/wrangler": { - "version": "3.114.17", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", - "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "version": "4.59.3", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.59.3.tgz", + "integrity": "sha512-zl+nqoGzWJ4K+NEMjy4GiaIi9ix59FkOzd7UsDb8CQADwy3li1DSNAzHty/BWYa3ZvMxr/G4pogMBb5vcSrNvQ==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { - "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/unenv-preset": "2.0.2", - "@esbuild-plugins/node-globals-polyfill": "0.2.3", - "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.10.0", "blake3-wasm": "2.1.5", - "esbuild": "0.17.19", - "miniflare": "3.20250718.3", + "esbuild": "0.27.0", + "miniflare": "4.20260116.0", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.14", - "workerd": "1.20250718.0" + "unenv": "2.0.0-rc.24", + "workerd": "1.20260116.0" }, "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" }, "engines": { - "node": ">=16.17.0" + "node": ">=20.0.0" }, "optionalDependencies": { - "fsevents": "~2.3.2", - "sharp": "^0.33.5" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250408.0" + "@cloudflare/workers-types": "^4.20260116.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -2538,10 +2429,27 @@ } } }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "cpu": [ "arm" ], @@ -2552,13 +2460,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "cpu": [ "arm64" ], @@ -2569,13 +2477,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "cpu": [ "x64" ], @@ -2586,13 +2494,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "cpu": [ "arm64" ], @@ -2603,13 +2511,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "cpu": [ "x64" ], @@ -2620,13 +2528,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "cpu": [ "arm64" ], @@ -2637,13 +2545,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "cpu": [ "x64" ], @@ -2654,13 +2562,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "cpu": [ "arm" ], @@ -2671,13 +2579,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "cpu": [ "arm64" ], @@ -2688,13 +2596,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "cpu": [ "ia32" ], @@ -2705,13 +2613,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "cpu": [ "loong64" ], @@ -2722,13 +2630,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "cpu": [ "mips64el" ], @@ -2739,13 +2647,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "cpu": [ "ppc64" ], @@ -2756,13 +2664,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "cpu": [ "riscv64" ], @@ -2773,13 +2681,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "cpu": [ "s390x" ], @@ -2790,13 +2698,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "cpu": [ "x64" ], @@ -2807,13 +2715,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "cpu": [ "x64" ], @@ -2824,13 +2732,13 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "cpu": [ "x64" ], @@ -2841,13 +2749,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "cpu": [ "x64" ], @@ -2858,13 +2766,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "cpu": [ "arm64" ], @@ -2875,13 +2783,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "cpu": [ "ia32" ], @@ -2892,13 +2800,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "cpu": [ "x64" ], @@ -2909,13 +2817,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/wrangler/node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2923,31 +2831,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" } }, "node_modules/ws": { @@ -2973,21 +2885,34 @@ } }, "node_modules/youch": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", "dev": true, "license": "MIT", "dependencies": { - "cookie": "^0.7.1", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" } }, "node_modules/zod": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index ab348ff..2a0e226 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,6 @@ "@cloudflare/workers-types": "^4.20241205.0", "typescript": "^5.7.2", "vitest": "^2.1.8", - "wrangler": "^3.99.0" + "wrangler": "^4.59.3" } } diff --git a/schema.sql b/schema.sql index c23fcd2..09499c9 100644 --- a/schema.sql +++ b/schema.sql @@ -87,6 +87,8 @@ CREATE TABLE IF NOT EXISTS pricing ( region_id INTEGER NOT NULL, hourly_price REAL NOT NULL, monthly_price REAL NOT NULL, + hourly_price_krw REAL, + monthly_price_krw REAL, currency TEXT NOT NULL DEFAULT 'USD', available INTEGER NOT NULL DEFAULT 1, -- boolean: 1=true, 0=false created_at TEXT NOT NULL DEFAULT (datetime('now')), @@ -176,6 +178,64 @@ BEGIN VALUES (NEW.id, NEW.hourly_price, NEW.monthly_price, datetime('now')); END; +-- ============================================================ +-- Table: gpu_instances +-- Description: GPU-specific instance types for specialized workloads +-- ============================================================ +CREATE TABLE IF NOT EXISTS gpu_instances ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider_id INTEGER NOT NULL, + instance_id TEXT NOT NULL, -- provider's instance identifier + instance_name TEXT NOT NULL, -- display name + vcpu INTEGER NOT NULL, + memory_mb INTEGER NOT NULL, + storage_gb INTEGER NOT NULL, + transfer_tb REAL, -- data transfer limit + network_speed_gbps REAL, + gpu_count INTEGER NOT NULL CHECK (gpu_count > 0), + gpu_type TEXT NOT NULL, -- e.g., "NVIDIA A100", "NVIDIA RTX6000" + gpu_memory_gb INTEGER, -- GPU memory per GPU in GB + metadata TEXT, -- JSON for additional provider-specific data + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE, + UNIQUE(provider_id, instance_id) +); + +-- Indexes for GPU instance queries +CREATE INDEX IF NOT EXISTS idx_gpu_instances_provider_id ON gpu_instances(provider_id); +CREATE INDEX IF NOT EXISTS idx_gpu_instances_gpu_type ON gpu_instances(gpu_type); +CREATE INDEX IF NOT EXISTS idx_gpu_instances_gpu_count ON gpu_instances(gpu_count); +CREATE INDEX IF NOT EXISTS idx_gpu_instances_provider_type ON gpu_instances(provider_id, gpu_type); + +-- ============================================================ +-- Table: gpu_pricing +-- Description: Region-specific pricing for GPU instance types +-- ============================================================ +CREATE TABLE IF NOT EXISTS gpu_pricing ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gpu_instance_id INTEGER NOT NULL, + region_id INTEGER NOT NULL, + hourly_price REAL NOT NULL, + monthly_price REAL NOT NULL, + hourly_price_krw REAL, + monthly_price_krw REAL, + currency TEXT NOT NULL DEFAULT 'USD', + available INTEGER NOT NULL DEFAULT 1, -- boolean: 1=true, 0=false + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (gpu_instance_id) REFERENCES gpu_instances(id) ON DELETE CASCADE, + FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE, + UNIQUE(gpu_instance_id, region_id) +); + +-- Indexes for GPU pricing queries +CREATE INDEX IF NOT EXISTS idx_gpu_pricing_instance_id ON gpu_pricing(gpu_instance_id); +CREATE INDEX IF NOT EXISTS idx_gpu_pricing_region_id ON gpu_pricing(region_id); +CREATE INDEX IF NOT EXISTS idx_gpu_pricing_hourly_price ON gpu_pricing(hourly_price); +CREATE INDEX IF NOT EXISTS idx_gpu_pricing_monthly_price ON gpu_pricing(monthly_price); +CREATE INDEX IF NOT EXISTS idx_gpu_pricing_available ON gpu_pricing(available); + -- ============================================================ -- Composite Indexes: Query Performance Optimization -- Description: Multi-column indexes to optimize common query patterns @@ -198,3 +258,161 @@ ON pricing(instance_type_id, region_id, hourly_price); -- Used in: Region filtering in main instance query CREATE INDEX IF NOT EXISTS idx_regions_provider_code ON regions(provider_id, region_code); + +-- ============================================================ +-- Triggers: GPU table auto-update timestamps +-- ============================================================ + +CREATE TRIGGER IF NOT EXISTS update_gpu_instances_updated_at +AFTER UPDATE ON gpu_instances +FOR EACH ROW +BEGIN + UPDATE gpu_instances SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +CREATE TRIGGER IF NOT EXISTS update_gpu_pricing_updated_at +AFTER UPDATE ON gpu_pricing +FOR EACH ROW +BEGIN + UPDATE gpu_pricing SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +-- ============================================================ +-- Table: g8_instances +-- Description: G8 generation Dedicated instances +-- ============================================================ +CREATE TABLE IF NOT EXISTS g8_instances ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider_id INTEGER NOT NULL, + instance_id TEXT NOT NULL, -- provider's instance identifier + instance_name TEXT NOT NULL, -- display name + vcpu INTEGER NOT NULL, + memory_mb INTEGER NOT NULL, + storage_gb INTEGER NOT NULL, + transfer_tb REAL, -- data transfer limit + network_speed_gbps REAL, + metadata TEXT, -- JSON for additional provider-specific data + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE, + UNIQUE(provider_id, instance_id) +); + +-- Indexes for G8 instance queries +CREATE INDEX IF NOT EXISTS idx_g8_instances_provider_id ON g8_instances(provider_id); +CREATE INDEX IF NOT EXISTS idx_g8_instances_instance_id ON g8_instances(instance_id); + +-- ============================================================ +-- Table: g8_pricing +-- Description: Region-specific pricing for G8 instance types +-- ============================================================ +CREATE TABLE IF NOT EXISTS g8_pricing ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + g8_instance_id INTEGER NOT NULL, + region_id INTEGER NOT NULL, + hourly_price REAL NOT NULL, + monthly_price REAL NOT NULL, + hourly_price_krw REAL, + monthly_price_krw REAL, + currency TEXT NOT NULL DEFAULT 'USD', + available INTEGER NOT NULL DEFAULT 1, -- boolean: 1=true, 0=false + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (g8_instance_id) REFERENCES g8_instances(id) ON DELETE CASCADE, + FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE, + UNIQUE(g8_instance_id, region_id) +); + +-- Indexes for G8 pricing queries +CREATE INDEX IF NOT EXISTS idx_g8_pricing_instance_id ON g8_pricing(g8_instance_id); +CREATE INDEX IF NOT EXISTS idx_g8_pricing_region_id ON g8_pricing(region_id); +CREATE INDEX IF NOT EXISTS idx_g8_pricing_hourly_price ON g8_pricing(hourly_price); +CREATE INDEX IF NOT EXISTS idx_g8_pricing_monthly_price ON g8_pricing(monthly_price); +CREATE INDEX IF NOT EXISTS idx_g8_pricing_available ON g8_pricing(available); + +-- ============================================================ +-- Table: vpu_instances +-- Description: VPU (NETINT Quadra) accelerated instances +-- ============================================================ +CREATE TABLE IF NOT EXISTS vpu_instances ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider_id INTEGER NOT NULL, + instance_id TEXT NOT NULL, -- provider's instance identifier + instance_name TEXT NOT NULL, -- display name + vcpu INTEGER NOT NULL, + memory_mb INTEGER NOT NULL, + storage_gb INTEGER NOT NULL, + transfer_tb REAL, -- data transfer limit + network_speed_gbps REAL, + vpu_type TEXT NOT NULL, -- e.g., "NETINT Quadra" + metadata TEXT, -- JSON for additional provider-specific data + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE, + UNIQUE(provider_id, instance_id) +); + +-- Indexes for VPU instance queries +CREATE INDEX IF NOT EXISTS idx_vpu_instances_provider_id ON vpu_instances(provider_id); +CREATE INDEX IF NOT EXISTS idx_vpu_instances_instance_id ON vpu_instances(instance_id); +CREATE INDEX IF NOT EXISTS idx_vpu_instances_vpu_type ON vpu_instances(vpu_type); + +-- ============================================================ +-- Table: vpu_pricing +-- Description: Region-specific pricing for VPU instance types +-- ============================================================ +CREATE TABLE IF NOT EXISTS vpu_pricing ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vpu_instance_id INTEGER NOT NULL, + region_id INTEGER NOT NULL, + hourly_price REAL NOT NULL, + monthly_price REAL NOT NULL, + hourly_price_krw REAL, + monthly_price_krw REAL, + currency TEXT NOT NULL DEFAULT 'USD', + available INTEGER NOT NULL DEFAULT 1, -- boolean: 1=true, 0=false + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (vpu_instance_id) REFERENCES vpu_instances(id) ON DELETE CASCADE, + FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE, + UNIQUE(vpu_instance_id, region_id) +); + +-- Indexes for VPU pricing queries +CREATE INDEX IF NOT EXISTS idx_vpu_pricing_instance_id ON vpu_pricing(vpu_instance_id); +CREATE INDEX IF NOT EXISTS idx_vpu_pricing_region_id ON vpu_pricing(region_id); +CREATE INDEX IF NOT EXISTS idx_vpu_pricing_hourly_price ON vpu_pricing(hourly_price); +CREATE INDEX IF NOT EXISTS idx_vpu_pricing_monthly_price ON vpu_pricing(monthly_price); +CREATE INDEX IF NOT EXISTS idx_vpu_pricing_available ON vpu_pricing(available); + +-- ============================================================ +-- Triggers: G8 and VPU table auto-update timestamps +-- ============================================================ + +CREATE TRIGGER IF NOT EXISTS update_g8_instances_updated_at +AFTER UPDATE ON g8_instances +FOR EACH ROW +BEGIN + UPDATE g8_instances SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +CREATE TRIGGER IF NOT EXISTS update_g8_pricing_updated_at +AFTER UPDATE ON g8_pricing +FOR EACH ROW +BEGIN + UPDATE g8_pricing SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +CREATE TRIGGER IF NOT EXISTS update_vpu_instances_updated_at +AFTER UPDATE ON vpu_instances +FOR EACH ROW +BEGIN + UPDATE vpu_instances SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +CREATE TRIGGER IF NOT EXISTS update_vpu_pricing_updated_at +AFTER UPDATE ON vpu_pricing +FOR EACH ROW +BEGIN + UPDATE vpu_pricing SET updated_at = datetime('now') WHERE id = NEW.id; +END; diff --git a/src/connectors/linode.ts b/src/connectors/linode.ts index 9e5d862..de08c5c 100644 --- a/src/connectors/linode.ts +++ b/src/connectors/linode.ts @@ -1,4 +1,4 @@ -import type { Env, RegionInput, InstanceTypeInput, InstanceFamily } from '../types'; +import type { Env, RegionInput, InstanceTypeInput, InstanceFamily, GpuInstanceInput, G8InstanceInput, VpuInstanceInput } from '../types'; import { VaultClient, VaultError } from './vault'; import { RateLimiter } from './base'; import { createLogger } from '../utils/logger'; @@ -186,6 +186,133 @@ export class LinodeConnector { }; } + /** + * Normalize Linode GPU instance type data for database storage + * + * @param raw - Raw Linode instance type data with GPU + * @param providerId - Database provider ID + * @returns Normalized GPU instance data ready for insertion + */ + normalizeGpuInstance(raw: LinodeInstanceType, providerId: number): GpuInstanceInput { + // Extract GPU memory from metadata if available + // Linode doesn't provide GPU memory in API, set to null + const gpuMemoryGb = null; + + return { + provider_id: providerId, + instance_id: raw.id, + instance_name: raw.label, + vcpu: raw.vcpus, + memory_mb: raw.memory, + storage_gb: Math.round(raw.disk / 1024), + transfer_tb: raw.transfer / 1000, + network_speed_gbps: raw.network_out / 1000, + gpu_count: raw.gpus, + gpu_type: this.extractGpuType(raw), + gpu_memory_gb: gpuMemoryGb, + metadata: JSON.stringify({ + class: raw.class, + hourly_price: raw.price.hourly, + monthly_price: raw.price.monthly, + }), + }; + } + + /** + * Normalize Linode G8 generation instance data for database storage + * + * @param raw - Raw Linode instance type data + * @param providerId - Database provider ID + * @returns Normalized G8 instance data ready for insertion + */ + normalizeG8Instance(raw: LinodeInstanceType, providerId: number): G8InstanceInput { + return { + provider_id: providerId, + instance_id: raw.id, + instance_name: raw.label, + vcpu: raw.vcpus, + memory_mb: raw.memory, + storage_gb: Math.round(raw.disk / 1024), + transfer_tb: raw.transfer / 1000, + network_speed_gbps: raw.network_out / 1000, + metadata: JSON.stringify({ + class: raw.class, + hourly_price: raw.price.hourly, + monthly_price: raw.price.monthly, + }), + }; + } + + /** + * Normalize Linode VPU instance type data for database storage + * + * @param raw - Raw Linode instance type data with VPU + * @param providerId - Database provider ID + * @returns Normalized VPU instance data ready for insertion + */ + normalizeVpuInstance(raw: LinodeInstanceType, providerId: number): VpuInstanceInput { + return { + provider_id: providerId, + instance_id: raw.id, + instance_name: raw.label, + vcpu: raw.vcpus, + memory_mb: raw.memory, + storage_gb: Math.round(raw.disk / 1024), + transfer_tb: raw.transfer / 1000, + network_speed_gbps: raw.network_out / 1000, + vpu_type: this.extractVpuType(raw), + metadata: JSON.stringify({ + class: raw.class, + hourly_price: raw.price.hourly, + monthly_price: raw.price.monthly, + }), + }; + } + + /** + * Extract GPU type from Linode instance data + * Linode uses NVIDIA GPUs, but specific model info may vary + * + * @param raw - Raw Linode instance type data + * @returns GPU type string + */ + private extractGpuType(raw: LinodeInstanceType): string { + // Check label for specific GPU model information + const labelLower = raw.label.toLowerCase(); + + if (labelLower.includes('rtx6000')) { + return 'NVIDIA RTX6000'; + } + if (labelLower.includes('a100')) { + return 'NVIDIA A100'; + } + if (labelLower.includes('v100')) { + return 'NVIDIA V100'; + } + if (labelLower.includes('rtx')) { + return 'NVIDIA RTX'; + } + + // Default to generic NVIDIA for GPU instances + return 'NVIDIA GPU'; + } + + /** + * Extract VPU type from Linode instance data + * + * @param raw - Raw Linode instance type data + * @returns VPU type string + */ + private extractVpuType(raw: LinodeInstanceType): string { + const labelLower = raw.label.toLowerCase(); + + if (labelLower.includes('netint')) { + return 'NETINT Quadra'; + } + + return 'VPU Accelerator'; + } + /** * Map Linode instance class to standard instance family * diff --git a/src/connectors/vultr.ts b/src/connectors/vultr.ts index 1fd5f71..1ee0bd6 100644 --- a/src/connectors/vultr.ts +++ b/src/connectors/vultr.ts @@ -1,4 +1,4 @@ -import type { Env, RegionInput, InstanceTypeInput, InstanceFamily } from '../types'; +import type { Env, RegionInput, InstanceTypeInput, InstanceFamily, GpuInstanceInput } from '../types'; import { VaultClient, VaultError } from './vault'; import { RateLimiter } from './base'; import { createLogger } from '../utils/logger'; @@ -210,6 +210,42 @@ export class VultrConnector { }; } + /** + * Normalize Vultr GPU plan data for database storage + * + * @param raw - Raw Vultr plan data for GPU instance (vcg type) + * @param providerId - Database provider ID + * @returns Normalized GPU instance data ready for insertion + */ + normalizeGpuInstance(raw: VultrPlan, providerId: number): GpuInstanceInput { + const hourlyPrice = raw.monthly_cost / 730; + + // Extract GPU type from vcg prefix + // vcg-* instances are NVIDIA-based GPU instances + const gpuType = 'NVIDIA'; + + return { + provider_id: providerId, + instance_id: raw.id, + instance_name: raw.id, + vcpu: raw.vcpu_count, + memory_mb: raw.ram, + storage_gb: raw.disk, + transfer_tb: raw.bandwidth / 1000, + network_speed_gbps: null, + gpu_count: 1, // Vultr vcg instances have 1 GPU + gpu_type: gpuType, + gpu_memory_gb: null, // Vultr doesn't expose GPU memory in plans API + metadata: JSON.stringify({ + type: raw.type, + disk_count: raw.disk_count, + locations: raw.locations, + hourly_price: hourlyPrice, + monthly_price: raw.monthly_cost, + }), + }; + } + /** * Map Vultr instance type to standard instance family * diff --git a/src/constants.ts b/src/constants.ts index a54b221..08b1e75 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -176,6 +176,7 @@ export const CORS = { ALLOWED_ORIGINS: [ 'https://anvil.it.com', 'https://cloud.anvil.it.com', + 'https://hosting.anvil.it.com', 'http://localhost:3000', // DEVELOPMENT ONLY - exclude in production ] as string[], /** Max age for CORS preflight cache (24 hours) */ @@ -223,3 +224,90 @@ export const REQUEST_LIMITS = { /** Maximum request body size in bytes (10KB) */ MAX_BODY_SIZE: 10 * 1024, } as const; + +// ============================================================ +// KRW Pricing Configuration +// ============================================================ + +/** + * Default KRW (Korean Won) pricing configuration + * + * These defaults are used when environment variables are not set. + * Calculation formula: + * KRW = USD × VAT (1.1) × Markup (1.1) × Exchange Rate (1450) + * KRW = USD × 1754.5 + */ +export const KRW_PRICING_DEFAULTS = { + /** VAT multiplier (10% VAT) */ + VAT_MULTIPLIER: 1.1, + /** Markup multiplier (10% markup) */ + MARKUP_MULTIPLIER: 1.1, + /** USD to KRW exchange rate */ + EXCHANGE_RATE: 1450, +} as const; + +/** + * KRW pricing configuration interface + */ +export interface KRWConfig { + exchangeRate: number; + vatRate: number; + markupRate: number; + totalMultiplier: number; +} + +/** + * Get KRW pricing configuration from environment variables + * Falls back to default values if env vars are not set + * + * @param env - Cloudflare Worker environment + * @returns KRW pricing configuration + */ +export function getKRWConfig(env?: { KRW_EXCHANGE_RATE?: string; KRW_VAT_RATE?: string; KRW_MARKUP_RATE?: string }): KRWConfig { + const exchangeRate = env?.KRW_EXCHANGE_RATE ? parseFloat(env.KRW_EXCHANGE_RATE) : KRW_PRICING_DEFAULTS.EXCHANGE_RATE; + const vatRate = env?.KRW_VAT_RATE ? parseFloat(env.KRW_VAT_RATE) : KRW_PRICING_DEFAULTS.VAT_MULTIPLIER; + const markupRate = env?.KRW_MARKUP_RATE ? parseFloat(env.KRW_MARKUP_RATE) : KRW_PRICING_DEFAULTS.MARKUP_MULTIPLIER; + + return { + exchangeRate, + vatRate, + markupRate, + totalMultiplier: vatRate * markupRate * exchangeRate, + }; +} + +/** + * Calculate KRW hourly price from USD price + * Applies VAT, markup, and exchange rate conversion + * + * @param usd - Hourly price in USD + * @param env - Optional environment for custom rates (uses defaults if not provided) + * @returns Price in KRW, rounded to nearest 1 KRW (minimum 1 KRW) + * + * @example + * calculateKRWHourly(0.0075) // Returns 13 (with defaults) + * calculateKRWHourly(0.144) // Returns 253 (with defaults) + */ +export function calculateKRWHourly(usd: number, env?: { KRW_EXCHANGE_RATE?: string; KRW_VAT_RATE?: string; KRW_MARKUP_RATE?: string }): number { + const config = getKRWConfig(env); + const krw = Math.round(usd * config.totalMultiplier); + return Math.max(krw, 1); +} + +/** + * Calculate KRW monthly price from USD price + * Applies VAT, markup, and exchange rate conversion + * + * @param usd - Monthly price in USD + * @param env - Optional environment for custom rates (uses defaults if not provided) + * @returns Price in KRW, rounded to nearest 100 KRW (minimum 100 KRW) + * + * @example + * calculateKRWMonthly(5) // Returns 8800 (with defaults) + * calculateKRWMonthly(96) // Returns 168400 (with defaults) + */ +export function calculateKRWMonthly(usd: number, env?: { KRW_EXCHANGE_RATE?: string; KRW_VAT_RATE?: string; KRW_MARKUP_RATE?: string }): number { + const config = getKRWConfig(env); + const krw = Math.round(usd * config.totalMultiplier / 100) * 100; + return Math.max(krw, 100); +} diff --git a/src/repositories/g8-instances.ts b/src/repositories/g8-instances.ts new file mode 100644 index 0000000..2e66ed0 --- /dev/null +++ b/src/repositories/g8-instances.ts @@ -0,0 +1,135 @@ +/** + * G8 Instances Repository + * Handles CRUD operations for G8 generation Dedicated instance types + */ + +import { BaseRepository } from './base'; +import { G8Instance, G8InstanceInput, RepositoryError, ErrorCodes } from '../types'; +import { createLogger } from '../utils/logger'; + +export class G8InstancesRepository extends BaseRepository { + protected tableName = 'g8_instances'; + protected logger = createLogger('[G8InstancesRepository]'); + protected allowedColumns = [ + 'provider_id', + 'instance_id', + 'instance_name', + 'vcpu', + 'memory_mb', + 'storage_gb', + 'transfer_tb', + 'network_speed_gbps', + 'metadata', + ]; + + /** + * Find all G8 instances for a specific provider + */ + async findByProvider(providerId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM g8_instances WHERE provider_id = ?') + .bind(providerId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByProvider failed', { + providerId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find G8 instances for provider: ${providerId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find G8 instance by provider and instance ID + */ + async findByInstanceId(providerId: number, instanceId: string): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM g8_instances WHERE provider_id = ? AND instance_id = ?') + .bind(providerId, instanceId) + .first(); + + return result || null; + } catch (error) { + this.logger.error('findByInstanceId failed', { + providerId, + instanceId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find G8 instance: ${instanceId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Upsert multiple G8 instances (batch operation) + */ + async upsertMany(providerId: number, instances: G8InstanceInput[]): Promise { + if (instances.length === 0) { + return 0; + } + + try { + const statements = instances.map((instance) => { + return this.db.prepare( + `INSERT INTO g8_instances ( + provider_id, instance_id, instance_name, vcpu, memory_mb, + storage_gb, transfer_tb, network_speed_gbps, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(provider_id, instance_id) + DO UPDATE SET + instance_name = excluded.instance_name, + vcpu = excluded.vcpu, + memory_mb = excluded.memory_mb, + storage_gb = excluded.storage_gb, + transfer_tb = excluded.transfer_tb, + network_speed_gbps = excluded.network_speed_gbps, + metadata = excluded.metadata, + updated_at = datetime('now')` + ).bind( + providerId, + instance.instance_id, + instance.instance_name, + instance.vcpu, + instance.memory_mb, + instance.storage_gb, + instance.transfer_tb, + instance.network_speed_gbps, + instance.metadata + ); + }); + + const results = await this.db.batch(statements); + const successCount = results.filter((r) => r.success).length; + + this.logger.info('upsertMany completed', { + providerId, + total: instances.length, + success: successCount, + }); + + return successCount; + } catch (error) { + this.logger.error('upsertMany failed', { + providerId, + count: instances.length, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to upsert G8 instances for provider: ${providerId}`, + ErrorCodes.TRANSACTION_FAILED, + error + ); + } + } +} diff --git a/src/repositories/g8-pricing.ts b/src/repositories/g8-pricing.ts new file mode 100644 index 0000000..0abbfc6 --- /dev/null +++ b/src/repositories/g8-pricing.ts @@ -0,0 +1,138 @@ +/** + * G8 Pricing Repository + * Handles CRUD operations for G8 instance pricing data + */ + +import { BaseRepository } from './base'; +import { G8Pricing, G8PricingInput, RepositoryError, ErrorCodes, Env } from '../types'; +import { createLogger } from '../utils/logger'; +import { calculateKRWHourly, calculateKRWMonthly } from '../constants'; + +export class G8PricingRepository extends BaseRepository { + protected tableName = 'g8_pricing'; + protected logger = createLogger('[G8PricingRepository]'); + protected allowedColumns = [ + 'g8_instance_id', + 'region_id', + 'hourly_price', + 'monthly_price', + 'hourly_price_krw', + 'monthly_price_krw', + 'currency', + 'available', + ]; + + constructor(db: D1Database, private env?: Env) { + super(db); + } + + /** + * Find all pricing records for a specific G8 instance + */ + async findByG8Instance(g8InstanceId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM g8_pricing WHERE g8_instance_id = ?') + .bind(g8InstanceId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByG8Instance failed', { + g8InstanceId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find pricing for G8 instance: ${g8InstanceId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find pricing for a specific G8 instance in a specific region + */ + async findByInstanceAndRegion(g8InstanceId: number, regionId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM g8_pricing WHERE g8_instance_id = ? AND region_id = ?') + .bind(g8InstanceId, regionId) + .first(); + + return result || null; + } catch (error) { + this.logger.error('findByInstanceAndRegion failed', { + g8InstanceId, + regionId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find pricing for G8 instance ${g8InstanceId} in region ${regionId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Upsert multiple G8 pricing records (batch operation) + */ + async upsertMany(pricingData: G8PricingInput[]): Promise { + if (pricingData.length === 0) { + return 0; + } + + try { + const statements = pricingData.map((pricing) => { + const hourlyKrw = calculateKRWHourly(pricing.hourly_price, this.env); + const monthlyKrw = calculateKRWMonthly(pricing.monthly_price, this.env); + + return this.db.prepare( + `INSERT INTO g8_pricing ( + g8_instance_id, region_id, hourly_price, monthly_price, + hourly_price_krw, monthly_price_krw, currency, available + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(g8_instance_id, region_id) + DO UPDATE SET + hourly_price = excluded.hourly_price, + monthly_price = excluded.monthly_price, + hourly_price_krw = excluded.hourly_price_krw, + monthly_price_krw = excluded.monthly_price_krw, + currency = excluded.currency, + available = excluded.available, + updated_at = datetime('now')` + ).bind( + pricing.g8_instance_id, + pricing.region_id, + pricing.hourly_price, + pricing.monthly_price, + hourlyKrw, + monthlyKrw, + pricing.currency, + pricing.available + ); + }); + + const results = await this.db.batch(statements); + const successCount = results.filter((r) => r.success).length; + + this.logger.info('upsertMany completed', { + total: pricingData.length, + success: successCount, + }); + + return successCount; + } catch (error) { + this.logger.error('upsertMany failed', { + count: pricingData.length, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + 'Failed to upsert G8 pricing records', + ErrorCodes.TRANSACTION_FAILED, + error + ); + } + } +} diff --git a/src/repositories/gpu-instances.ts b/src/repositories/gpu-instances.ts new file mode 100644 index 0000000..e2c6de5 --- /dev/null +++ b/src/repositories/gpu-instances.ts @@ -0,0 +1,274 @@ +/** + * GPU Instances Repository + * Handles CRUD operations for GPU-specific instance types + */ + +import { BaseRepository } from './base'; +import { GpuInstance, GpuInstanceInput, RepositoryError, ErrorCodes } from '../types'; +import { createLogger } from '../utils/logger'; + +export class GpuInstancesRepository extends BaseRepository { + protected tableName = 'gpu_instances'; + protected logger = createLogger('[GpuInstancesRepository]'); + protected allowedColumns = [ + 'provider_id', + 'instance_id', + 'instance_name', + 'vcpu', + 'memory_mb', + 'storage_gb', + 'transfer_tb', + 'network_speed_gbps', + 'gpu_count', + 'gpu_type', + 'gpu_memory_gb', + 'metadata', + ]; + + /** + * Find all GPU instances for a specific provider + */ + async findByProvider(providerId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM gpu_instances WHERE provider_id = ?') + .bind(providerId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByProvider failed', { + providerId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find GPU instances for provider: ${providerId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find GPU instances by GPU type + */ + async findByGpuType(gpuType: string): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM gpu_instances WHERE gpu_type = ?') + .bind(gpuType) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByGpuType failed', { + gpuType, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find GPU instances by type: ${gpuType}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find a GPU instance by provider ID and instance ID + */ + async findByInstanceId(providerId: number, instanceId: string): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM gpu_instances WHERE provider_id = ? AND instance_id = ?') + .bind(providerId, instanceId) + .first(); + + return result || null; + } catch (error) { + this.logger.error('findByInstanceId failed', { + providerId, + instanceId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find GPU instance: ${instanceId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Bulk upsert GPU instances for a provider + * Uses batch operations for efficiency + */ + async upsertMany(providerId: number, instances: GpuInstanceInput[]): Promise { + if (instances.length === 0) { + return 0; + } + + try { + // Build upsert statements for each GPU instance + const statements = instances.map((instance) => { + return this.db.prepare( + `INSERT INTO gpu_instances ( + provider_id, instance_id, instance_name, vcpu, memory_mb, + storage_gb, transfer_tb, network_speed_gbps, gpu_count, + gpu_type, gpu_memory_gb, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(provider_id, instance_id) + DO UPDATE SET + instance_name = excluded.instance_name, + vcpu = excluded.vcpu, + memory_mb = excluded.memory_mb, + storage_gb = excluded.storage_gb, + transfer_tb = excluded.transfer_tb, + network_speed_gbps = excluded.network_speed_gbps, + gpu_count = excluded.gpu_count, + gpu_type = excluded.gpu_type, + gpu_memory_gb = excluded.gpu_memory_gb, + metadata = excluded.metadata` + ).bind( + providerId, + instance.instance_id, + instance.instance_name, + instance.vcpu, + instance.memory_mb, + instance.storage_gb, + instance.transfer_tb || null, + instance.network_speed_gbps || null, + instance.gpu_count, + instance.gpu_type, + instance.gpu_memory_gb || null, + instance.metadata || null + ); + }); + + const results = await this.executeBatch(statements); + + // Count successful operations + const successCount = results.reduce( + (sum, result) => sum + (result.meta.changes ?? 0), + 0 + ); + + this.logger.info('Upserted GPU instances', { + providerId, + count: successCount + }); + return successCount; + } catch (error) { + this.logger.error('upsertMany failed', { + providerId, + count: instances.length, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to upsert GPU instances for provider: ${providerId}`, + ErrorCodes.TRANSACTION_FAILED, + error + ); + } + } + + /** + * Search GPU instances by specifications + */ + async search(criteria: { + providerId?: number; + minVcpu?: number; + maxVcpu?: number; + minMemoryMb?: number; + maxMemoryMb?: number; + minGpuCount?: number; + gpuType?: string; + minGpuMemoryGb?: number; + }): Promise { + try { + const conditions: string[] = []; + const params: (string | number | boolean | null)[] = []; + + if (criteria.providerId !== undefined) { + conditions.push('provider_id = ?'); + params.push(criteria.providerId); + } + + if (criteria.minVcpu !== undefined) { + conditions.push('vcpu >= ?'); + params.push(criteria.minVcpu); + } + + if (criteria.maxVcpu !== undefined) { + conditions.push('vcpu <= ?'); + params.push(criteria.maxVcpu); + } + + if (criteria.minMemoryMb !== undefined) { + conditions.push('memory_mb >= ?'); + params.push(criteria.minMemoryMb); + } + + if (criteria.maxMemoryMb !== undefined) { + conditions.push('memory_mb <= ?'); + params.push(criteria.maxMemoryMb); + } + + if (criteria.minGpuCount !== undefined) { + conditions.push('gpu_count >= ?'); + params.push(criteria.minGpuCount); + } + + if (criteria.gpuType !== undefined) { + conditions.push('gpu_type = ?'); + params.push(criteria.gpuType); + } + + if (criteria.minGpuMemoryGb !== undefined) { + conditions.push('gpu_memory_gb >= ?'); + params.push(criteria.minGpuMemoryGb); + } + + const whereClause = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : ''; + const query = 'SELECT * FROM gpu_instances' + whereClause; + + const result = await this.db + .prepare(query) + .bind(...params) + .all(); + + return result.results; + } catch (error) { + this.logger.error('search failed', { + criteria, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + 'Failed to search GPU instances', + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Get distinct GPU types available in the database + */ + async getAvailableGpuTypes(): Promise { + try { + const result = await this.db + .prepare('SELECT DISTINCT gpu_type FROM gpu_instances ORDER BY gpu_type') + .all<{ gpu_type: string }>(); + + return result.results.map(row => row.gpu_type); + } catch (error) { + this.logger.error('getAvailableGpuTypes failed', { + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + 'Failed to get available GPU types', + ErrorCodes.DATABASE_ERROR, + error + ); + } + } +} diff --git a/src/repositories/gpu-pricing.ts b/src/repositories/gpu-pricing.ts new file mode 100644 index 0000000..228e9ec --- /dev/null +++ b/src/repositories/gpu-pricing.ts @@ -0,0 +1,223 @@ +/** + * GPU Pricing Repository + * Handles CRUD operations for GPU instance pricing data + */ + +import { BaseRepository } from './base'; +import { GpuPricing, GpuPricingInput, RepositoryError, ErrorCodes, Env } from '../types'; +import { createLogger } from '../utils/logger'; +import { calculateKRWHourly, calculateKRWMonthly } from '../constants'; + +export class GpuPricingRepository extends BaseRepository { + protected tableName = 'gpu_pricing'; + protected logger = createLogger('[GpuPricingRepository]'); + protected allowedColumns = [ + 'gpu_instance_id', + 'region_id', + 'hourly_price', + 'monthly_price', + 'hourly_price_krw', + 'monthly_price_krw', + 'currency', + 'available', + ]; + + constructor(db: D1Database, private env?: Env) { + super(db); + } + + /** + * Find pricing for a specific GPU instance + */ + async findByGpuInstance(gpuInstanceId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM gpu_pricing WHERE gpu_instance_id = ?') + .bind(gpuInstanceId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByGpuInstance failed', { + gpuInstanceId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find pricing for GPU instance: ${gpuInstanceId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find pricing for a specific region + */ + async findByRegion(regionId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM gpu_pricing WHERE region_id = ?') + .bind(regionId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByRegion failed', { + regionId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find GPU pricing for region: ${regionId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find specific pricing record by GPU instance and region + */ + async findByGpuInstanceAndRegion( + gpuInstanceId: number, + regionId: number + ): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM gpu_pricing WHERE gpu_instance_id = ? AND region_id = ?') + .bind(gpuInstanceId, regionId) + .first(); + + return result || null; + } catch (error) { + this.logger.error('findByGpuInstanceAndRegion failed', { + gpuInstanceId, + regionId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find GPU pricing for instance ${gpuInstanceId} in region ${regionId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Bulk upsert GPU pricing records + * Uses batch operations for efficiency + */ + async upsertMany(pricingData: GpuPricingInput[]): Promise { + if (pricingData.length === 0) { + return 0; + } + + try { + const statements = pricingData.map((pricing) => { + const hourlyKrw = calculateKRWHourly(pricing.hourly_price, this.env); + const monthlyKrw = calculateKRWMonthly(pricing.monthly_price, this.env); + + return this.db.prepare( + `INSERT INTO gpu_pricing ( + gpu_instance_id, region_id, hourly_price, monthly_price, + hourly_price_krw, monthly_price_krw, currency, available + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(gpu_instance_id, region_id) + DO UPDATE SET + hourly_price = excluded.hourly_price, + monthly_price = excluded.monthly_price, + hourly_price_krw = excluded.hourly_price_krw, + monthly_price_krw = excluded.monthly_price_krw, + currency = excluded.currency, + available = excluded.available` + ).bind( + pricing.gpu_instance_id, + pricing.region_id, + pricing.hourly_price, + pricing.monthly_price, + hourlyKrw, + monthlyKrw, + pricing.currency, + pricing.available + ); + }); + + const results = await this.executeBatch(statements); + + const successCount = results.reduce( + (sum, result) => sum + (result.meta.changes ?? 0), + 0 + ); + + this.logger.info('Upserted GPU pricing records', { count: successCount }); + return successCount; + } catch (error) { + this.logger.error('upsertMany failed', { + count: pricingData.length, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + 'Failed to upsert GPU pricing records', + ErrorCodes.TRANSACTION_FAILED, + error + ); + } + } + + /** + * Search GPU pricing by price range + */ + async searchByPriceRange( + minHourly?: number, + maxHourly?: number, + minMonthly?: number, + maxMonthly?: number + ): Promise { + try { + const conditions: string[] = []; + const params: (string | number | boolean | null)[] = []; + + if (minHourly !== undefined) { + conditions.push('hourly_price >= ?'); + params.push(minHourly); + } + + if (maxHourly !== undefined) { + conditions.push('hourly_price <= ?'); + params.push(maxHourly); + } + + if (minMonthly !== undefined) { + conditions.push('monthly_price >= ?'); + params.push(minMonthly); + } + + if (maxMonthly !== undefined) { + conditions.push('monthly_price <= ?'); + params.push(maxMonthly); + } + + const whereClause = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : ''; + const query = 'SELECT * FROM gpu_pricing' + whereClause + ' ORDER BY hourly_price'; + + const result = await this.db + .prepare(query) + .bind(...params) + .all(); + + return result.results; + } catch (error) { + this.logger.error('searchByPriceRange failed', { + minHourly, + maxHourly, + minMonthly, + maxMonthly, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + 'Failed to search GPU pricing by price range', + ErrorCodes.DATABASE_ERROR, + error + ); + } + } +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts index ed10f97..f26f32d 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -8,11 +8,24 @@ export { ProvidersRepository } from './providers'; export { RegionsRepository } from './regions'; export { InstancesRepository } from './instances'; export { PricingRepository } from './pricing'; +export { GpuInstancesRepository } from './gpu-instances'; +export { GpuPricingRepository } from './gpu-pricing'; +export { G8InstancesRepository } from './g8-instances'; +export { G8PricingRepository } from './g8-pricing'; +export { VpuInstancesRepository } from './vpu-instances'; +export { VpuPricingRepository } from './vpu-pricing'; import { ProvidersRepository } from './providers'; import { RegionsRepository } from './regions'; import { InstancesRepository } from './instances'; import { PricingRepository } from './pricing'; +import { GpuInstancesRepository } from './gpu-instances'; +import { GpuPricingRepository } from './gpu-pricing'; +import { G8InstancesRepository } from './g8-instances'; +import { G8PricingRepository } from './g8-pricing'; +import { VpuInstancesRepository } from './vpu-instances'; +import { VpuPricingRepository } from './vpu-pricing'; +import type { Env } from '../types'; /** * Repository factory for creating repository instances @@ -23,8 +36,14 @@ export class RepositoryFactory { private _regions?: RegionsRepository; private _instances?: InstancesRepository; private _pricing?: PricingRepository; + private _gpuInstances?: GpuInstancesRepository; + private _gpuPricing?: GpuPricingRepository; + private _g8Instances?: G8InstancesRepository; + private _g8Pricing?: G8PricingRepository; + private _vpuInstances?: VpuInstancesRepository; + private _vpuPricing?: VpuPricingRepository; - constructor(private _db: D1Database) {} + constructor(private _db: D1Database, private _env?: Env) {} /** * Access to raw D1 database instance for advanced operations (e.g., batch queries) @@ -33,6 +52,13 @@ export class RepositoryFactory { return this._db; } + /** + * Access to environment variables for KRW pricing configuration + */ + get env(): Env | undefined { + return this._env; + } + get providers(): ProvidersRepository { return this._providers ??= new ProvidersRepository(this.db); } @@ -46,6 +72,30 @@ export class RepositoryFactory { } get pricing(): PricingRepository { - return this._pricing ??= new PricingRepository(this.db); + return this._pricing ??= new PricingRepository(this.db, this._env); + } + + get gpuInstances(): GpuInstancesRepository { + return this._gpuInstances ??= new GpuInstancesRepository(this.db); + } + + get gpuPricing(): GpuPricingRepository { + return this._gpuPricing ??= new GpuPricingRepository(this.db, this._env); + } + + get g8Instances(): G8InstancesRepository { + return this._g8Instances ??= new G8InstancesRepository(this.db); + } + + get g8Pricing(): G8PricingRepository { + return this._g8Pricing ??= new G8PricingRepository(this.db, this._env); + } + + get vpuInstances(): VpuInstancesRepository { + return this._vpuInstances ??= new VpuInstancesRepository(this.db); + } + + get vpuPricing(): VpuPricingRepository { + return this._vpuPricing ??= new VpuPricingRepository(this.db, this._env); } } diff --git a/src/repositories/pricing.ts b/src/repositories/pricing.ts index e023a5c..f3255d3 100644 --- a/src/repositories/pricing.ts +++ b/src/repositories/pricing.ts @@ -4,8 +4,9 @@ */ import { BaseRepository } from './base'; -import { Pricing, PricingInput, PriceHistory, RepositoryError, ErrorCodes } from '../types'; +import { Pricing, PricingInput, PriceHistory, RepositoryError, ErrorCodes, Env } from '../types'; import { createLogger } from '../utils/logger'; +import { calculateKRWHourly, calculateKRWMonthly } from '../constants'; export class PricingRepository extends BaseRepository { protected tableName = 'pricing'; @@ -15,10 +16,16 @@ export class PricingRepository extends BaseRepository { 'region_id', 'hourly_price', 'monthly_price', + 'hourly_price_krw', + 'monthly_price_krw', 'currency', 'available', ]; + constructor(db: D1Database, private env?: Env) { + super(db); + } + /** * Find pricing records for a specific instance type */ @@ -107,15 +114,20 @@ export class PricingRepository extends BaseRepository { try { // Build upsert statements for each pricing record const statements = pricing.map((price) => { + const hourlyKrw = calculateKRWHourly(price.hourly_price, this.env); + const monthlyKrw = calculateKRWMonthly(price.monthly_price, this.env); + return this.db.prepare( `INSERT INTO pricing ( - instance_type_id, region_id, hourly_price, monthly_price, - currency, available - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(instance_type_id, region_id) + instance_type_id, region_id, hourly_price, monthly_price, + hourly_price_krw, monthly_price_krw, currency, available + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(instance_type_id, region_id) DO UPDATE SET hourly_price = excluded.hourly_price, monthly_price = excluded.monthly_price, + hourly_price_krw = excluded.hourly_price_krw, + monthly_price_krw = excluded.monthly_price_krw, currency = excluded.currency, available = excluded.available` ).bind( @@ -123,6 +135,8 @@ export class PricingRepository extends BaseRepository { price.region_id, price.hourly_price, price.monthly_price, + hourlyKrw, + monthlyKrw, price.currency, price.available ); diff --git a/src/repositories/vpu-instances.ts b/src/repositories/vpu-instances.ts new file mode 100644 index 0000000..962b193 --- /dev/null +++ b/src/repositories/vpu-instances.ts @@ -0,0 +1,162 @@ +/** + * VPU Instances Repository + * Handles CRUD operations for VPU (NETINT Quadra) accelerated instance types + */ + +import { BaseRepository } from './base'; +import { VpuInstance, VpuInstanceInput, RepositoryError, ErrorCodes } from '../types'; +import { createLogger } from '../utils/logger'; + +export class VpuInstancesRepository extends BaseRepository { + protected tableName = 'vpu_instances'; + protected logger = createLogger('[VpuInstancesRepository]'); + protected allowedColumns = [ + 'provider_id', + 'instance_id', + 'instance_name', + 'vcpu', + 'memory_mb', + 'storage_gb', + 'transfer_tb', + 'network_speed_gbps', + 'vpu_type', + 'metadata', + ]; + + /** + * Find all VPU instances for a specific provider + */ + async findByProvider(providerId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM vpu_instances WHERE provider_id = ?') + .bind(providerId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByProvider failed', { + providerId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find VPU instances for provider: ${providerId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find VPU instance by provider and instance ID + */ + async findByInstanceId(providerId: number, instanceId: string): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM vpu_instances WHERE provider_id = ? AND instance_id = ?') + .bind(providerId, instanceId) + .first(); + + return result || null; + } catch (error) { + this.logger.error('findByInstanceId failed', { + providerId, + instanceId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find VPU instance: ${instanceId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find VPU instances by VPU type + */ + async findByVpuType(vpuType: string): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM vpu_instances WHERE vpu_type = ?') + .bind(vpuType) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByVpuType failed', { + vpuType, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find VPU instances by type: ${vpuType}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Upsert multiple VPU instances (batch operation) + */ + async upsertMany(providerId: number, instances: VpuInstanceInput[]): Promise { + if (instances.length === 0) { + return 0; + } + + try { + const statements = instances.map((instance) => { + return this.db.prepare( + `INSERT INTO vpu_instances ( + provider_id, instance_id, instance_name, vcpu, memory_mb, + storage_gb, transfer_tb, network_speed_gbps, vpu_type, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(provider_id, instance_id) + DO UPDATE SET + instance_name = excluded.instance_name, + vcpu = excluded.vcpu, + memory_mb = excluded.memory_mb, + storage_gb = excluded.storage_gb, + transfer_tb = excluded.transfer_tb, + network_speed_gbps = excluded.network_speed_gbps, + vpu_type = excluded.vpu_type, + metadata = excluded.metadata, + updated_at = datetime('now')` + ).bind( + providerId, + instance.instance_id, + instance.instance_name, + instance.vcpu, + instance.memory_mb, + instance.storage_gb, + instance.transfer_tb, + instance.network_speed_gbps, + instance.vpu_type, + instance.metadata + ); + }); + + const results = await this.db.batch(statements); + const successCount = results.filter((r) => r.success).length; + + this.logger.info('upsertMany completed', { + providerId, + total: instances.length, + success: successCount, + }); + + return successCount; + } catch (error) { + this.logger.error('upsertMany failed', { + providerId, + count: instances.length, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to upsert VPU instances for provider: ${providerId}`, + ErrorCodes.TRANSACTION_FAILED, + error + ); + } + } +} diff --git a/src/repositories/vpu-pricing.ts b/src/repositories/vpu-pricing.ts new file mode 100644 index 0000000..e349934 --- /dev/null +++ b/src/repositories/vpu-pricing.ts @@ -0,0 +1,138 @@ +/** + * VPU Pricing Repository + * Handles CRUD operations for VPU instance pricing data + */ + +import { BaseRepository } from './base'; +import { VpuPricing, VpuPricingInput, RepositoryError, ErrorCodes, Env } from '../types'; +import { createLogger } from '../utils/logger'; +import { calculateKRWHourly, calculateKRWMonthly } from '../constants'; + +export class VpuPricingRepository extends BaseRepository { + protected tableName = 'vpu_pricing'; + protected logger = createLogger('[VpuPricingRepository]'); + protected allowedColumns = [ + 'vpu_instance_id', + 'region_id', + 'hourly_price', + 'monthly_price', + 'hourly_price_krw', + 'monthly_price_krw', + 'currency', + 'available', + ]; + + constructor(db: D1Database, private env?: Env) { + super(db); + } + + /** + * Find all pricing records for a specific VPU instance + */ + async findByVpuInstance(vpuInstanceId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM vpu_pricing WHERE vpu_instance_id = ?') + .bind(vpuInstanceId) + .all(); + + return result.results; + } catch (error) { + this.logger.error('findByVpuInstance failed', { + vpuInstanceId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find pricing for VPU instance: ${vpuInstanceId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Find pricing for a specific VPU instance in a specific region + */ + async findByInstanceAndRegion(vpuInstanceId: number, regionId: number): Promise { + try { + const result = await this.db + .prepare('SELECT * FROM vpu_pricing WHERE vpu_instance_id = ? AND region_id = ?') + .bind(vpuInstanceId, regionId) + .first(); + + return result || null; + } catch (error) { + this.logger.error('findByInstanceAndRegion failed', { + vpuInstanceId, + regionId, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + `Failed to find pricing for VPU instance ${vpuInstanceId} in region ${regionId}`, + ErrorCodes.DATABASE_ERROR, + error + ); + } + } + + /** + * Upsert multiple VPU pricing records (batch operation) + */ + async upsertMany(pricingData: VpuPricingInput[]): Promise { + if (pricingData.length === 0) { + return 0; + } + + try { + const statements = pricingData.map((pricing) => { + const hourlyKrw = calculateKRWHourly(pricing.hourly_price, this.env); + const monthlyKrw = calculateKRWMonthly(pricing.monthly_price, this.env); + + return this.db.prepare( + `INSERT INTO vpu_pricing ( + vpu_instance_id, region_id, hourly_price, monthly_price, + hourly_price_krw, monthly_price_krw, currency, available + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(vpu_instance_id, region_id) + DO UPDATE SET + hourly_price = excluded.hourly_price, + monthly_price = excluded.monthly_price, + hourly_price_krw = excluded.hourly_price_krw, + monthly_price_krw = excluded.monthly_price_krw, + currency = excluded.currency, + available = excluded.available, + updated_at = datetime('now')` + ).bind( + pricing.vpu_instance_id, + pricing.region_id, + pricing.hourly_price, + pricing.monthly_price, + hourlyKrw, + monthlyKrw, + pricing.currency, + pricing.available + ); + }); + + const results = await this.db.batch(statements); + const successCount = results.filter((r) => r.success).length; + + this.logger.info('upsertMany completed', { + total: pricingData.length, + success: successCount, + }); + + return successCount; + } catch (error) { + this.logger.error('upsertMany failed', { + count: pricingData.length, + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw new RepositoryError( + 'Failed to upsert VPU pricing records', + ErrorCodes.TRANSACTION_FAILED, + error + ); + } + } +} diff --git a/src/services/query.ts b/src/services/query.ts index 0be0158..816be61 100644 --- a/src/services/query.ts +++ b/src/services/query.ts @@ -64,6 +64,8 @@ interface RawQueryResult { pricing_region_id: number | null; hourly_price: number | null; monthly_price: number | null; + hourly_price_krw: number | null; + monthly_price_krw: number | null; currency: string | null; pricing_available: number | null; pricing_created_at: string | null; @@ -190,6 +192,8 @@ export class QueryService { pr.region_id as pricing_region_id, pr.hourly_price, pr.monthly_price, + pr.hourly_price_krw, + pr.monthly_price_krw, pr.currency, pr.available as pricing_available, pr.created_at as pricing_created_at, @@ -376,6 +380,8 @@ export class QueryService { region_id: row.pricing_region_id, hourly_price: row.hourly_price, monthly_price: row.monthly_price, + hourly_price_krw: row.hourly_price_krw, + monthly_price_krw: row.monthly_price_krw, currency: row.currency, available: row.pricing_available, created_at: row.pricing_created_at, diff --git a/src/services/sync.ts b/src/services/sync.ts index f5fd37b..0587bdd 100644 --- a/src/services/sync.ts +++ b/src/services/sync.ts @@ -25,6 +25,8 @@ import type { RegionInput, InstanceTypeInput, PricingInput, + GpuInstanceInput, + GpuPricingInput, } from '../types'; import { SyncStage } from '../types'; @@ -46,17 +48,32 @@ export interface SyncConnectorAdapter { /** Fetch all instance types (normalized) */ getInstanceTypes(): Promise; + /** Fetch GPU instances (optional, only for providers with GPU support) */ + getGpuInstances?(): Promise; + + /** Fetch G8 instances (optional, only for Linode) */ + getG8Instances?(): Promise; + + /** Fetch VPU instances (optional, only for Linode) */ + getVpuInstances?(): Promise; + /** * Fetch pricing data for instances and regions * @param instanceTypeIds - Array of database instance type IDs * @param regionIds - Array of database region IDs * @param dbInstanceMap - Map of DB instance type ID to instance_id (API ID) for avoiding redundant queries + * @param dbGpuMap - Map of GPU instance IDs (optional) + * @param dbG8Map - Map of G8 instance IDs (optional) + * @param dbVpuMap - Map of VPU instance IDs (optional) * @returns Array of pricing records OR number of records if batched internally */ getPricing( instanceTypeIds: number[], regionIds: number[], - dbInstanceMap: Map + dbInstanceMap: Map, + dbGpuMap?: Map, + dbG8Map?: Map, + dbVpuMap?: Map ): Promise; } @@ -73,7 +90,7 @@ export class SyncOrchestrator { private vault: VaultClient, env?: Env ) { - this.repos = new RepositoryFactory(db); + this.repos = new RepositoryFactory(db, env); this.env = env; this.logger = createLogger('[SyncOrchestrator]', env); this.logger.info('Initialized'); @@ -138,17 +155,79 @@ export class SyncOrchestrator { providerRecord.id, normalizedRegions ); - const instancesCount = await this.repos.instances.upsertMany( + + // Persist regular instances (already filtered in getInstanceTypes) + const regularInstancesCount = await this.repos.instances.upsertMany( providerRecord.id, normalizedInstances ); + // Handle specialized instances separately for Linode and Vultr + let gpuInstancesCount = 0; + let g8InstancesCount = 0; + let vpuInstancesCount = 0; + + if (provider.toLowerCase() === 'linode') { + // GPU instances + if ('getGpuInstances' in connector) { + const gpuInstances = await (connector as any).getGpuInstances(); + if (gpuInstances && gpuInstances.length > 0) { + gpuInstancesCount = await this.repos.gpuInstances.upsertMany( + providerRecord.id, + gpuInstances + ); + } + } + + // G8 instances + if ('getG8Instances' in connector) { + const g8Instances = await (connector as any).getG8Instances(); + if (g8Instances && g8Instances.length > 0) { + g8InstancesCount = await this.repos.g8Instances.upsertMany( + providerRecord.id, + g8Instances + ); + } + } + + // VPU instances + if ('getVpuInstances' in connector) { + const vpuInstances = await (connector as any).getVpuInstances(); + if (vpuInstances && vpuInstances.length > 0) { + vpuInstancesCount = await this.repos.vpuInstances.upsertMany( + providerRecord.id, + vpuInstances + ); + } + } + } + + // Handle Vultr GPU instances + if (provider.toLowerCase() === 'vultr') { + if ('getGpuInstances' in connector) { + const gpuInstances = await (connector as any).getGpuInstances(); + if (gpuInstances && gpuInstances.length > 0) { + gpuInstancesCount = await this.repos.gpuInstances.upsertMany( + providerRecord.id, + gpuInstances + ); + } + } + } + + const instancesCount = regularInstancesCount + gpuInstancesCount + g8InstancesCount + vpuInstancesCount; + // Fetch pricing data - need instance and region IDs from DB - // Use D1 batch to reduce query count from 2 to 1 (50% reduction in queries) - const [dbRegionsResult, dbInstancesResult] = await this.repos.db.batch([ + // Use D1 batch to reduce query count (fetch all instance types in one batch) + const batchQueries = [ this.repos.db.prepare('SELECT id, region_code FROM regions WHERE provider_id = ?').bind(providerRecord.id), - this.repos.db.prepare('SELECT id, instance_id FROM instance_types WHERE provider_id = ?').bind(providerRecord.id) - ]); + this.repos.db.prepare('SELECT id, instance_id FROM instance_types WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM gpu_instances WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM g8_instances WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM vpu_instances WHERE provider_id = ?').bind(providerRecord.id) + ]; + + const [dbRegionsResult, dbInstancesResult, dbGpuResult, dbG8Result, dbVpuResult] = await this.repos.db.batch(batchQueries); if (!dbRegionsResult.success || !dbInstancesResult.success) { throw new Error('Failed to fetch regions/instances for pricing'); @@ -164,8 +243,27 @@ export class SyncOrchestrator { dbInstancesData.map(i => [i.id, { instance_id: i.instance_id }]) ); + // Create specialized instance mappings + const dbGpuMap = new Map( + (dbGpuResult.results as Array<{ id: number; instance_id: string }>).map(i => [i.id, { instance_id: i.instance_id }]) + ); + const dbG8Map = new Map( + (dbG8Result.results as Array<{ id: number; instance_id: string }>).map(i => [i.id, { instance_id: i.instance_id }]) + ); + const dbVpuMap = new Map( + (dbVpuResult.results as Array<{ id: number; instance_id: string }>).map(i => [i.id, { instance_id: i.instance_id }]) + ); + // Get pricing data - may return array or count depending on provider - const pricingResult = await connector.getPricing(instanceTypeIds, regionIds, dbInstanceMap); + // Pass all instance maps for specialized pricing + const pricingResult = await connector.getPricing( + instanceTypeIds, + regionIds, + dbInstanceMap, + dbGpuMap, + dbG8Map, + dbVpuMap + ); // Handle both return types: array (Linode, Vultr) or number (AWS with generator) let pricingCount = 0; @@ -177,7 +275,15 @@ export class SyncOrchestrator { pricingCount = await this.repos.pricing.upsertMany(pricingResult); } - this.logger.info(`${provider} → ${stage}`, { regions: regionsCount, instances: instancesCount, pricing: pricingCount }); + this.logger.info(`${provider} → ${stage}`, { + regions: regionsCount, + regular_instances: regularInstancesCount, + gpu_instances: gpuInstancesCount, + g8_instances: g8InstancesCount, + vpu_instances: vpuInstancesCount, + total_instances: instancesCount, + pricing: pricingCount + }); // Stage 7: Validate stage = SyncStage.VALIDATE; @@ -497,6 +603,235 @@ export class SyncOrchestrator { } } + /** + * Generate Linode GPU pricing records in batches using Generator pattern + * Minimizes memory usage by yielding batches at a time (default: 100) + * + * @param gpuInstanceTypeIds - Array of database GPU instance type IDs + * @param regionIds - Array of database region IDs + * @param dbGpuInstanceMap - Map of GPU instance type ID to DB instance data + * @param rawInstanceMap - Map of instance_id (API ID) to raw Linode data + * @param env - Environment configuration for SYNC_BATCH_SIZE + * @yields Batches of GpuPricingInput records (configurable batch size) + * + * Manual Test: + * For typical Linode GPU instances (~10 GPU types × 20 regions = 200 records): + * - Default batch size (100): ~2 batches + * - Memory savings: ~50% (200 records → 100 records in memory) + * - Verify: Check logs for "Generated and upserted GPU pricing records for Linode" + */ + private *generateLinodeGpuPricingBatches( + gpuInstanceTypeIds: number[], + regionIds: number[], + dbGpuInstanceMap: Map, + rawInstanceMap: Map, + env?: Env + ): Generator { + const BATCH_SIZE = parseInt(env?.SYNC_BATCH_SIZE || '100', 10); + let batch: GpuPricingInput[] = []; + + for (const regionId of regionIds) { + for (const gpuInstanceId of gpuInstanceTypeIds) { + const dbInstance = dbGpuInstanceMap.get(gpuInstanceId); + if (!dbInstance) { + this.logger.warn('GPU instance type not found', { gpuInstanceId }); + continue; + } + + const rawInstance = rawInstanceMap.get(dbInstance.instance_id); + if (!rawInstance) { + this.logger.warn('Raw GPU instance data not found', { instance_id: dbInstance.instance_id }); + continue; + } + + batch.push({ + gpu_instance_id: gpuInstanceId, + region_id: regionId, + hourly_price: rawInstance.price.hourly, + monthly_price: rawInstance.price.monthly, + currency: 'USD', + available: 1, + }); + + if (batch.length >= BATCH_SIZE) { + yield batch; + batch = []; + } + } + } + + // Yield remaining records + if (batch.length > 0) { + yield batch; + } + } + + /** + * Generate Vultr GPU pricing records in batches using Generator pattern + * Minimizes memory usage by yielding batches at a time (default: 100) + * + * @param gpuInstanceTypeIds - Array of database GPU instance type IDs + * @param regionIds - Array of database region IDs + * @param dbGpuInstanceMap - Map of GPU instance type ID to DB instance data + * @param rawPlanMap - Map of plan_id (API ID) to raw Vultr plan data + * @param env - Environment configuration for SYNC_BATCH_SIZE + * @yields Batches of GpuPricingInput records (configurable batch size) + * + * Manual Test: + * For typical Vultr GPU instances (~35 vcg types × 20 regions = 700 records): + * - Default batch size (100): ~7 batches + * - Memory savings: ~85% (700 records → 100 records in memory) + * - Verify: Check logs for "Generated and upserted GPU pricing records for Vultr" + */ + private *generateVultrGpuPricingBatches( + gpuInstanceTypeIds: number[], + regionIds: number[], + dbGpuInstanceMap: Map, + rawPlanMap: Map, + env?: Env + ): Generator { + const BATCH_SIZE = parseInt(env?.SYNC_BATCH_SIZE || '100', 10); + let batch: GpuPricingInput[] = []; + + for (const regionId of regionIds) { + for (const gpuInstanceId of gpuInstanceTypeIds) { + const dbInstance = dbGpuInstanceMap.get(gpuInstanceId); + if (!dbInstance) { + this.logger.warn('GPU instance type not found', { gpuInstanceId }); + continue; + } + + const rawPlan = rawPlanMap.get(dbInstance.instance_id); + if (!rawPlan) { + this.logger.warn('Raw GPU plan data not found', { instance_id: dbInstance.instance_id }); + continue; + } + + // Calculate hourly price: monthly_cost / 730 hours + const hourlyPrice = rawPlan.monthly_cost / 730; + + batch.push({ + gpu_instance_id: gpuInstanceId, + region_id: regionId, + hourly_price: hourlyPrice, + monthly_price: rawPlan.monthly_cost, + currency: 'USD', + available: 1, + }); + + if (batch.length >= BATCH_SIZE) { + yield batch; + batch = []; + } + } + } + + // Yield remaining records + if (batch.length > 0) { + yield batch; + } + } + + /** + * Generate G8 pricing records in batches for Linode + * Similar to GPU pricing generator but for G8 instances + */ + private *generateLinodeG8PricingBatches( + g8InstanceTypeIds: number[], + regionIds: number[], + dbG8InstanceMap: Map, + rawInstanceMap: Map, + env?: Env + ): Generator { + const BATCH_SIZE = parseInt(env?.SYNC_BATCH_SIZE || '100', 10); + let batch: any[] = []; + + for (const regionId of regionIds) { + for (const g8InstanceId of g8InstanceTypeIds) { + const dbInstance = dbG8InstanceMap.get(g8InstanceId); + if (!dbInstance) { + this.logger.warn('G8 instance type not found', { g8InstanceId }); + continue; + } + + const rawInstance = rawInstanceMap.get(dbInstance.instance_id); + if (!rawInstance) { + this.logger.warn('Raw G8 instance data not found', { instance_id: dbInstance.instance_id }); + continue; + } + + batch.push({ + g8_instance_id: g8InstanceId, + region_id: regionId, + hourly_price: rawInstance.price.hourly, + monthly_price: rawInstance.price.monthly, + currency: 'USD', + available: 1, + }); + + if (batch.length >= BATCH_SIZE) { + yield batch; + batch = []; + } + } + } + + // Yield remaining records + if (batch.length > 0) { + yield batch; + } + } + + /** + * Generate VPU pricing records in batches for Linode + * Similar to GPU pricing generator but for VPU instances + */ + private *generateLinodeVpuPricingBatches( + vpuInstanceTypeIds: number[], + regionIds: number[], + dbVpuInstanceMap: Map, + rawInstanceMap: Map, + env?: Env + ): Generator { + const BATCH_SIZE = parseInt(env?.SYNC_BATCH_SIZE || '100', 10); + let batch: any[] = []; + + for (const regionId of regionIds) { + for (const vpuInstanceId of vpuInstanceTypeIds) { + const dbInstance = dbVpuInstanceMap.get(vpuInstanceId); + if (!dbInstance) { + this.logger.warn('VPU instance type not found', { vpuInstanceId }); + continue; + } + + const rawInstance = rawInstanceMap.get(dbInstance.instance_id); + if (!rawInstance) { + this.logger.warn('Raw VPU instance data not found', { instance_id: dbInstance.instance_id }); + continue; + } + + batch.push({ + vpu_instance_id: vpuInstanceId, + region_id: regionId, + hourly_price: rawInstance.price.hourly, + monthly_price: rawInstance.price.monthly, + currency: 'USD', + available: 1, + }); + + if (batch.length >= BATCH_SIZE) { + yield batch; + batch = []; + } + } + } + + // Yield remaining records + if (batch.length > 0) { + yield batch; + } + } + /** * Create connector for a specific provider * @@ -521,20 +856,73 @@ export class SyncOrchestrator { getInstanceTypes: async () => { const instances = await connector.fetchInstanceTypes(); cachedInstanceTypes = instances; // Cache for pricing - return instances.map(i => connector.normalizeInstance(i, providerId)); + + // Classification priority: + // 1. GPU (gpus > 0) → handled in getGpuInstances + // 2. VPU (id contains 'netint' or 'accelerated') → handled in getVpuInstances + // 3. G8 (id starts with 'g8-') → handled in getG8Instances + // 4. Default → regular instance_types + const regularInstances = instances.filter(i => { + if (i.gpus > 0) return false; + if (i.id.includes('netint') || i.id.includes('accelerated')) return false; + if (i.id.startsWith('g8-')) return false; + return true; + }); + return regularInstances.map(i => connector.normalizeInstance(i, providerId)); + }, + getGpuInstances: async (): Promise => { + // Use cached instances if available to avoid redundant API calls + if (!cachedInstanceTypes) { + this.logger.info('Fetching instance types for GPU extraction'); + cachedInstanceTypes = await connector.fetchInstanceTypes(); + } + + // Filter and normalize GPU instances + const gpuInstances = cachedInstanceTypes.filter(i => i.gpus > 0); + return gpuInstances.map(i => connector.normalizeGpuInstance(i, providerId)); + }, + getG8Instances: async (): Promise => { + // Use cached instances if available to avoid redundant API calls + if (!cachedInstanceTypes) { + this.logger.info('Fetching instance types for G8 extraction'); + cachedInstanceTypes = await connector.fetchInstanceTypes(); + } + + // Filter and normalize G8 instances (g8- prefix) + const g8Instances = cachedInstanceTypes.filter(i => + i.id.startsWith('g8-') && (!i.gpus || i.gpus === 0) + ); + return g8Instances.map(i => connector.normalizeG8Instance(i, providerId)); + }, + getVpuInstances: async (): Promise => { + // Use cached instances if available to avoid redundant API calls + if (!cachedInstanceTypes) { + this.logger.info('Fetching instance types for VPU extraction'); + cachedInstanceTypes = await connector.fetchInstanceTypes(); + } + + // Filter and normalize VPU instances (netint or accelerated) + const vpuInstances = cachedInstanceTypes.filter(i => + (i.id.includes('netint') || i.id.includes('accelerated')) && (!i.gpus || i.gpus === 0) + ); + return vpuInstances.map(i => connector.normalizeVpuInstance(i, providerId)); }, getPricing: async ( - instanceTypeIds: number[], + _instanceTypeIds: number[], regionIds: number[], - dbInstanceMap: Map + dbInstanceMap: Map, + dbGpuMap?: Map, + dbG8Map?: Map, + dbVpuMap?: Map ): Promise => { /** * Linode Pricing Extraction Strategy (Generator Pattern): * * Linode pricing is embedded in instance type data (price.hourly, price.monthly). * Generate all region × instance combinations using generator pattern. + * GPU instances are separated and stored in gpu_pricing table. * - * Expected volume: ~200 instances × 20 regions = ~4,000 pricing records + * Expected volume: ~190 regular + ~10 GPU instances × 20 regions = ~4,000 pricing records * Generator pattern with 100 records/batch minimizes memory usage * Each batch is immediately persisted to database to avoid memory buildup * @@ -542,9 +930,9 @@ export class SyncOrchestrator { * * Manual Test: * 1. Run sync: curl -X POST http://localhost:8787/api/sync/linode - * 2. Verify pricing count: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM pricing WHERE instance_type_id IN (SELECT id FROM instance_types WHERE provider_id = (SELECT id FROM providers WHERE name = 'linode'))" - * 3. Sample pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT p.*, i.instance_name, r.region_code FROM pricing p JOIN instance_types i ON p.instance_type_id = i.id JOIN regions r ON p.region_id = r.id WHERE i.provider_id = (SELECT id FROM providers WHERE name = 'linode') LIMIT 10" - * 4. Verify data integrity: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM pricing WHERE hourly_price = 0 OR monthly_price = 0" + * 2. Verify regular pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM pricing WHERE instance_type_id IN (SELECT id FROM instance_types WHERE provider_id = (SELECT id FROM providers WHERE name = 'linode'))" + * 3. Verify GPU pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM gpu_pricing WHERE gpu_instance_id IN (SELECT id FROM gpu_instances WHERE provider_id = (SELECT id FROM providers WHERE name = 'linode'))" + * 4. Sample pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT p.*, i.instance_name, r.region_code FROM pricing p JOIN instance_types i ON p.instance_type_id = i.id JOIN regions r ON p.region_id = r.id WHERE i.provider_id = (SELECT id FROM providers WHERE name = 'linode') LIMIT 10" */ // Re-fetch instance types if not cached @@ -558,23 +946,113 @@ export class SyncOrchestrator { cachedInstanceTypes.map(i => [i.id, i]) ); - // Use generator pattern for memory-efficient processing - const pricingGenerator = this.generateLinodePricingBatches( - instanceTypeIds, - regionIds, - dbInstanceMap, - rawInstanceMap, - this.env - ); + // Use provided maps or create empty ones + const gpuMap = dbGpuMap || new Map(); + const g8Map = dbG8Map || new Map(); + const vpuMap = dbVpuMap || new Map(); - // Process batches incrementally - let totalCount = 0; - for (const batch of pricingGenerator) { - const batchCount = await this.repos.pricing.upsertMany(batch); - totalCount += batchCount; + // Separate instances by type: GPU, VPU, G8, and regular + const gpuInstanceTypeIds: number[] = []; + const g8InstanceTypeIds: number[] = []; + const vpuInstanceTypeIds: number[] = []; + const regularInstanceTypeIds: number[] = []; + + // Extract GPU instance IDs from gpuMap + for (const dbId of gpuMap.keys()) { + gpuInstanceTypeIds.push(dbId); } - this.logger.info('Generated and upserted pricing records for Linode', { count: totalCount }); + // Extract G8 instance IDs from g8Map + for (const dbId of g8Map.keys()) { + g8InstanceTypeIds.push(dbId); + } + + // Extract VPU instance IDs from vpuMap + for (const dbId of vpuMap.keys()) { + vpuInstanceTypeIds.push(dbId); + } + + // Regular instances from dbInstanceMap + for (const dbId of dbInstanceMap.keys()) { + regularInstanceTypeIds.push(dbId); + } + + // Process regular instance pricing + let regularPricingCount = 0; + if (regularInstanceTypeIds.length > 0) { + const regularGenerator = this.generateLinodePricingBatches( + regularInstanceTypeIds, + regionIds, + dbInstanceMap, + rawInstanceMap, + this.env + ); + + for (const batch of regularGenerator) { + const batchCount = await this.repos.pricing.upsertMany(batch); + regularPricingCount += batchCount; + } + } + + // Process GPU instance pricing + let gpuPricingCount = 0; + if (gpuInstanceTypeIds.length > 0) { + const gpuGenerator = this.generateLinodeGpuPricingBatches( + gpuInstanceTypeIds, + regionIds, + gpuMap, + rawInstanceMap, + this.env + ); + + for (const batch of gpuGenerator) { + const batchCount = await this.repos.gpuPricing.upsertMany(batch); + gpuPricingCount += batchCount; + } + } + + // Process G8 instance pricing + let g8PricingCount = 0; + if (g8InstanceTypeIds.length > 0) { + const g8Generator = this.generateLinodeG8PricingBatches( + g8InstanceTypeIds, + regionIds, + g8Map, + rawInstanceMap, + this.env + ); + + for (const batch of g8Generator) { + const batchCount = await this.repos.g8Pricing.upsertMany(batch); + g8PricingCount += batchCount; + } + } + + // Process VPU instance pricing + let vpuPricingCount = 0; + if (vpuInstanceTypeIds.length > 0) { + const vpuGenerator = this.generateLinodeVpuPricingBatches( + vpuInstanceTypeIds, + regionIds, + vpuMap, + rawInstanceMap, + this.env + ); + + for (const batch of vpuGenerator) { + const batchCount = await this.repos.vpuPricing.upsertMany(batch); + vpuPricingCount += batchCount; + } + } + + const totalCount = regularPricingCount + gpuPricingCount + g8PricingCount + vpuPricingCount; + this.logger.info('Generated and upserted pricing records for Linode', { + regular_pricing: regularPricingCount, + gpu_pricing: gpuPricingCount, + g8_pricing: g8PricingCount, + vpu_pricing: vpuPricingCount, + total: totalCount + }); // Return total count of processed records return totalCount; @@ -596,12 +1074,27 @@ export class SyncOrchestrator { getInstanceTypes: async () => { const plans = await connector.fetchPlans(); cachedPlans = plans; // Cache for pricing - return plans.map(p => connector.normalizeInstance(p, providerId)); + + // Filter out GPU instances (vcg type) + const regularPlans = plans.filter(p => !p.id.startsWith('vcg')); + return regularPlans.map(p => connector.normalizeInstance(p, providerId)); + }, + getGpuInstances: async (): Promise => { + // Use cached plans if available to avoid redundant API calls + if (!cachedPlans) { + this.logger.info('Fetching plans for GPU extraction'); + cachedPlans = await connector.fetchPlans(); + } + + // Filter and normalize GPU instances (vcg type) + const gpuPlans = cachedPlans.filter(p => p.id.startsWith('vcg')); + return gpuPlans.map(p => connector.normalizeGpuInstance(p, providerId)); }, getPricing: async ( instanceTypeIds: number[], regionIds: number[], - dbInstanceMap: Map + dbInstanceMap: Map, + dbGpuMap?: Map ): Promise => { /** * Vultr Pricing Extraction Strategy (Generator Pattern): @@ -609,17 +1102,19 @@ export class SyncOrchestrator { * Vultr pricing is embedded in plan data (monthly_cost). * Generate all region × plan combinations using generator pattern. * - * Expected volume: ~100 plans × 20 regions = ~2,000 pricing records + * Expected volume: ~100 regular plans × 20 regions = ~2,000 pricing records + * ~35 GPU plans × 20 regions = ~700 GPU pricing records * Generator pattern with 100 records/batch minimizes memory usage * Each batch is immediately persisted to database to avoid memory buildup * - * Memory savings: ~95% (2,000 records → 100 records in memory at a time) + * Memory savings: ~95% (2,700 records → 100 records in memory at a time) * * Manual Test: * 1. Run sync: curl -X POST http://localhost:8787/api/sync/vultr * 2. Verify pricing count: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM pricing WHERE instance_type_id IN (SELECT id FROM instance_types WHERE provider_id = (SELECT id FROM providers WHERE name = 'vultr'))" - * 3. Sample pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT p.*, i.instance_name, r.region_code FROM pricing p JOIN instance_types i ON p.instance_type_id = i.id JOIN regions r ON p.region_id = r.id WHERE i.provider_id = (SELECT id FROM providers WHERE name = 'vultr') LIMIT 10" - * 4. Verify data integrity: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM pricing WHERE hourly_price = 0 OR monthly_price = 0" + * 3. Verify GPU pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM gpu_pricing WHERE gpu_instance_id IN (SELECT id FROM gpu_instances WHERE provider_id = (SELECT id FROM providers WHERE name = 'vultr'))" + * 4. Sample pricing: wrangler d1 execute cloud-instances-db --local --command "SELECT p.*, i.instance_name, r.region_code FROM pricing p JOIN instance_types i ON p.instance_type_id = i.id JOIN regions r ON p.region_id = r.id WHERE i.provider_id = (SELECT id FROM providers WHERE name = 'vultr') LIMIT 10" + * 5. Verify data integrity: wrangler d1 execute cloud-instances-db --local --command "SELECT COUNT(*) FROM pricing WHERE hourly_price = 0 OR monthly_price = 0" */ // Re-fetch plans if not cached @@ -633,23 +1128,48 @@ export class SyncOrchestrator { cachedPlans.map(p => [p.id, p]) ); - // Use generator pattern for memory-efficient processing - const pricingGenerator = this.generateVultrPricingBatches( - instanceTypeIds, - regionIds, - dbInstanceMap, - rawPlanMap, - this.env - ); + // Process regular instance pricing + let regularPricingCount = 0; + if (instanceTypeIds.length > 0) { + const regularGenerator = this.generateVultrPricingBatches( + instanceTypeIds, + regionIds, + dbInstanceMap, + rawPlanMap, + this.env + ); - // Process batches incrementally - let totalCount = 0; - for (const batch of pricingGenerator) { - const batchCount = await this.repos.pricing.upsertMany(batch); - totalCount += batchCount; + for (const batch of regularGenerator) { + const batchCount = await this.repos.pricing.upsertMany(batch); + regularPricingCount += batchCount; + } } - this.logger.info('Generated and upserted pricing records for Vultr', { count: totalCount }); + // Process GPU instance pricing + let gpuPricingCount = 0; + const gpuMap = dbGpuMap || new Map(); + if (gpuMap.size > 0) { + const gpuInstanceTypeIds = Array.from(gpuMap.keys()); + const gpuGenerator = this.generateVultrGpuPricingBatches( + gpuInstanceTypeIds, + regionIds, + gpuMap, + rawPlanMap, + this.env + ); + + for (const batch of gpuGenerator) { + const batchCount = await this.repos.gpuPricing.upsertMany(batch); + gpuPricingCount += batchCount; + } + } + + const totalCount = regularPricingCount + gpuPricingCount; + this.logger.info('Generated and upserted pricing records for Vultr', { + regular_pricing: regularPricingCount, + gpu_pricing: gpuPricingCount, + total: totalCount + }); // Return total count of processed records return totalCount; diff --git a/src/types.ts b/src/types.ts index ae62ee4..6a2f0aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,6 +88,8 @@ export interface Pricing { region_id: number; hourly_price: number; monthly_price: number; + hourly_price_krw: number | null; + monthly_price_krw: number | null; currency: string; available: number; // SQLite boolean (0/1) created_at: string; @@ -102,6 +104,97 @@ export interface PriceHistory { recorded_at: string; } +export interface GpuInstance { + id: number; + provider_id: number; + instance_id: string; + instance_name: string; + vcpu: number; + memory_mb: number; + storage_gb: number; + transfer_tb: number | null; + network_speed_gbps: number | null; + gpu_count: number; + gpu_type: string; + gpu_memory_gb: number | null; + metadata: string | null; // JSON string + created_at: string; + updated_at: string; +} + +export interface GpuPricing { + id: number; + gpu_instance_id: number; + region_id: number; + hourly_price: number; + monthly_price: number; + hourly_price_krw: number | null; + monthly_price_krw: number | null; + currency: string; + available: number; // SQLite boolean (0/1) + created_at: string; + updated_at: string; +} + +export interface G8Instance { + id: number; + provider_id: number; + instance_id: string; + instance_name: string; + vcpu: number; + memory_mb: number; + storage_gb: number; + transfer_tb: number | null; + network_speed_gbps: number | null; + metadata: string | null; // JSON string + created_at: string; + updated_at: string; +} + +export interface G8Pricing { + id: number; + g8_instance_id: number; + region_id: number; + hourly_price: number; + monthly_price: number; + hourly_price_krw: number | null; + monthly_price_krw: number | null; + currency: string; + available: number; // SQLite boolean (0/1) + created_at: string; + updated_at: string; +} + +export interface VpuInstance { + id: number; + provider_id: number; + instance_id: string; + instance_name: string; + vcpu: number; + memory_mb: number; + storage_gb: number; + transfer_tb: number | null; + network_speed_gbps: number | null; + vpu_type: string; + metadata: string | null; // JSON string + created_at: string; + updated_at: string; +} + +export interface VpuPricing { + id: number; + vpu_instance_id: number; + region_id: number; + hourly_price: number; + monthly_price: number; + hourly_price_krw: number | null; + monthly_price_krw: number | null; + currency: string; + available: number; // SQLite boolean (0/1) + created_at: string; + updated_at: string; +} + // ============================================================ // Repository Input Types (for create/update operations) // ============================================================ @@ -109,7 +202,13 @@ export interface PriceHistory { export type ProviderInput = Omit; export type RegionInput = Omit; export type InstanceTypeInput = Omit; -export type PricingInput = Omit; +export type PricingInput = Omit; +export type GpuInstanceInput = Omit; +export type GpuPricingInput = Omit; +export type G8InstanceInput = Omit; +export type G8PricingInput = Omit; +export type VpuInstanceInput = Omit; +export type VpuPricingInput = Omit; // ============================================================ // Error Types @@ -353,6 +452,12 @@ export interface Env { LOG_LEVEL?: string; /** CORS origin for Access-Control-Allow-Origin header (default: '*') */ CORS_ORIGIN?: string; + /** KRW exchange rate (USD to KRW conversion, default: 1450) */ + KRW_EXCHANGE_RATE?: string; + /** KRW VAT rate multiplier (default: 1.1 for 10% VAT) */ + KRW_VAT_RATE?: string; + /** KRW markup rate multiplier (default: 1.1 for 10% markup) */ + KRW_MARKUP_RATE?: string; } // ============================================================ diff --git a/wrangler.toml b/wrangler.toml index 83e0347..9a558c7 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -19,7 +19,12 @@ VAULT_URL = "https://vault.anvil.it.com" SYNC_BATCH_SIZE = "100" CACHE_TTL_SECONDS = "300" LOG_LEVEL = "info" -CORS_ORIGIN = "https://anvil.it.com" +CORS_ORIGIN = "*" + +# KRW Pricing Configuration (can be changed without redeployment) +KRW_EXCHANGE_RATE = "1450" +KRW_VAT_RATE = "1.1" +KRW_MARKUP_RATE = "1.1" # Cron Triggers [triggers]