# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Cloudflare Worker-based AI server recommendation service. Uses OpenAI GPT-4o-mini (via AI Gateway), D1 database, KV cache, and VPS benchmark data to recommend cost-effective servers based on natural language requirements. **Production URL**: `https://cloud-orchestrator.kappa-d8e.workers.dev` ## Commands ```bash # Development npm run dev # Start local development server (wrangler dev) npm run deploy # Deploy to Cloudflare Workers npm run typecheck # TypeScript type checking npm test # Run tests (Vitest) npm run test:watch # Run tests in watch mode # Database operations (D1) npx wrangler d1 execute cloud-instances-db --file=schema.sql # Apply schema npx wrangler d1 execute cloud-instances-db --file=seed.sql # Seed data npx wrangler d1 execute cloud-instances-db --file=fix-tech-specs.sql # Update tech specs npx wrangler d1 execute cloud-instances-db --command="SELECT ..." # Ad-hoc queries # View logs npx wrangler tail ``` ## Architecture ``` src/ ├── index.ts # Main router, CORS, Queue consumer ├── config.ts # Configuration constants (LIMITS, TIMEOUTS, i18n) ├── types.ts # TypeScript type definitions ├── region-utils.ts # Region matching utilities ├── utils.ts # Re-exports from utils/ (backward compatibility) ├── utils/ # Modular utilities │ ├── index.ts # Central export point │ ├── http.ts # HTTP responses, CORS, escapeHtml │ ├── validation.ts # Input validation, type guards, escapeLikePattern │ ├── bandwidth.ts # Bandwidth estimation & cost calculation │ ├── cache.ts # Caching, rate limiting (with time-based cleanup) │ ├── ai.ts # AI prompt sanitization │ └── exchange-rate.ts # Currency conversion ├── middleware/ # Request middleware │ ├── index.ts # Central export point │ ├── auth.ts # API key authentication │ ├── origin.ts # Origin validation (restricts to .kappa-d8e.workers.dev) │ ├── rate-limit.ts # Rate limiting middleware │ ├── request-id.ts # Request ID generation │ ├── security.ts # Security headers (CSP, HSTS, etc.) │ └── user-id.ts # User ID extraction ├── repositories/ │ ├── AnvilServerRepository.ts # DB queries for Anvil servers │ └── ProvisioningRepository.ts # Users, deposits, orders (telegram-conversations) ├── services/ │ ├── ai-service.ts # AI recommendations & fallback logic │ ├── provisioning-service.ts # Server provisioning workflow + SSH key injection │ ├── vps-provider.ts # VPS provider abstract base class │ ├── linode-provider.ts # Linode API implementation │ └── vultr-provider.ts # Vultr API implementation ├── handlers/ │ ├── health.ts # GET /api/health │ ├── servers.ts # GET /api/servers │ ├── recommend.ts # POST /api/recommend │ ├── report.ts # GET /api/recommend/report (CSP: style-src 'unsafe-inline') │ ├── provision.ts # POST/GET/DELETE /api/provision/* │ └── queue.ts # Queue consumer for async provisioning └── __tests__/ ├── utils.test.ts # Validation & security tests (27 tests) └── bandwidth.test.ts # Bandwidth estimation tests (32 tests, including CDN) ``` ### Key Data Flow 1. User sends request (`tech_stack`, `expected_users`, `use_case`, `region_preference`) 2. Tech specs calculation with **DB workload multiplier** based on use_case 3. Candidate filtering with **flexible region matching** 4. VPS benchmarks retrieval (Geekbench 6), **prioritizing same provider** 5. AI analysis returns 3 tiers: Budget, Balanced, Premium 6. Results cached in KV (5 min TTL, empty results not cached) ### D1 Database Tables (cloud-instances-db) **Primary tables (Anvil pricing)**: - `anvil_instances` - Anvil server specifications (vcpus, memory_gb, disk_gb, transfer_tb, etc.) - `anvil_regions` - Anvil data center regions (name, display_name, country_code) - `anvil_pricing` - Anvil pricing data (monthly_price in USD) - `anvil_transfer_pricing` - Transfer/bandwidth overage pricing by region (price_per_gb in USD) **Support tables**: - `tech_specs` - Resource requirements per technology (vcpu_per_users, min_memory_mb) - `vps_benchmarks` - Geekbench 6 benchmark data (269 records) - `benchmark_results` / `benchmark_types` / `processors` - Phoronix benchmark data **Legacy tables (no longer used)**: - `providers`, `instance_types`, `pricing`, `regions` - Old Linode/Vultr data ### D1 Database Tables (telegram-conversations) **User & Payment tables**: - `users` - Telegram users (id, telegram_id, username) - `user_deposits` - User balance in KRW (user_id, balance) - `server_orders` - Server provisioning orders (status, price_paid, provider_instance_id, ip_address) ## Key Implementation Details ### DB Workload Multiplier (`recommend.ts`) Database resource requirements vary by workload type, not just user count: | Workload Type | Multiplier | Example Use Cases | |---------------|------------|-------------------| | Heavy | 0.3x | analytics, log server, reporting, dashboard | | Medium-Heavy | 0.5x | e-commerce, ERP, CRM, community forum | | Medium | 0.7x | API, SaaS, app backend | | Light | 1.0x | blog, portfolio, documentation, wiki | **Example**: PostgreSQL (vcpu_per_users: 200) with 1000 users - Analytics dashboard: 1000 / (200 × 0.3) = 17 vCPU - E-commerce: 1000 / (200 × 0.5) = 10 vCPU - Personal blog: 1000 / (200 × 1.0) = 5 vCPU ### Bandwidth Estimation (`bandwidth.ts`) Estimates monthly bandwidth based on use_case patterns: | Pattern | Page Size | Pages/Day | Active Ratio | |---------|-----------|-----------|--------------| | E-commerce | 2.5MB | 20 | 40% | | Streaming | 50MB | 5 | 20% | | Analytics | 0.7MB | 30 | 50% | | Blog/Content | 1.5MB | 4 | 30% | Heavy bandwidth (>1TB/month) triggers warning about overage costs. ### CDN Cache Hit Rate (`bandwidth.ts`) CDN 사용 시 원본 서버 트래픽을 자동 계산하여 실제 비용을 추정합니다. **API Parameters**: - `cdn_enabled`: CDN 사용 여부 (기본: `true`) - `cdn_cache_hit_rate`: 캐시 히트율 override (0.0-1.0, 기본: use_case별 자동) **Use Case별 기본 캐시 히트율**: | Use Case | 캐시 히트율 | 설명 | |----------|-------------|------| | video/streaming | 92% | 동일 영상 반복 시청 | | blog/static | 90% | 대부분 정적 콘텐츠 | | file/download | 85% | 소프트웨어/에셋 다운로드 | | ecommerce | 70% | 상품 이미지 캐시 가능 | | forum | 60% | 정적/동적 혼합 | | api/saas | 30% | 동적 응답 위주 | | gaming | 20% | 실시간 통신 | | chat | 10% | 실시간 메시지 | **응답 필드** (`bandwidth_estimate`): - `cdn_enabled`: CDN 사용 여부 - `cdn_cache_hit_rate`: 적용된 캐시 히트율 - `gross_monthly_tb`: CDN 적용 전 총 트래픽 - `origin_monthly_tb`: CDN 적용 후 원본 서버 트래픽 **응답 필드** (`bandwidth_info`): - `cdn_savings_tb`: CDN으로 절감된 트래픽 (TB) - `cdn_savings_cost`: CDN으로 절감된 비용 ($) **비용 절감 예시** (10,000 동시접속 video streaming): ``` CDN 없음: 총 2,966TB → 원본 2,966TB → 초과비용 $18,370 CDN 92%: 총 2,966TB → 원본 237TB → 초과비용 $1,464 절감: 2,729TB 절감, $16,906 절감 (92%) ``` ### Flexible Region Matching Both `/api/recommend` and `/api/servers` use `buildFlexibleRegionConditionsAnvil()` for anvil_regions: ```sql LOWER(ar.name) = ? OR LOWER(ar.name) LIKE ? OR LOWER(ar.display_name) LIKE ? OR LOWER(ar.country_code) = ? ``` Valid inputs: `"korea"`, `"KR"`, `"seoul"`, `"tokyo"`, `"japan"`, `"ap-northeast-2"`, `"icn"` Country names are auto-expanded via `COUNTRY_NAME_TO_REGIONS` mapping. ### Exchange Rate Handling (`utils.ts`) - Korean users (lang=ko) see prices in KRW - Exchange rate fetched from open.er-api.com with 1-hour KV cache - Fallback rate: 1450 KRW/USD if API unavailable ### Transfer Pricing (`bandwidth_info`) Each recommendation includes `bandwidth_info` with transfer/bandwidth cost details: | Field | Description | KRW Rounding | |-------|-------------|--------------| | `included_transfer_tb` | Free bandwidth included in plan (TB/month) | - | | `overage_cost_per_gb` | Overage cost per GB | 1원 단위 | | `overage_cost_per_tb` | Overage cost per TB | 100원 단위 | | `estimated_monthly_tb` | Estimated monthly usage (TB) | - | | `estimated_overage_tb` | Estimated overage (TB) | - | | `estimated_overage_cost` | Estimated overage charges | 100원 단위 | | `total_estimated_cost` | Server + overage total | 100원 단위 | | `currency` | "USD" or "KRW" | - | **Data sources**: - `included_transfer_tb`: From `anvil_instances.transfer_tb` - `overage_cost_per_gb`: From `anvil_transfer_pricing.price_per_gb` ### HTML Report Endpoint (`handlers/report.ts`) `GET /api/recommend/report?data={base64}&lang={en|ko}` Generates printable/PDF-friendly HTML report from recommendation results. **Parameters**: - `data`: Base64-encoded JSON of recommendation response - `lang`: Language (en, ko, ja, zh) - defaults to 'en' **Usage**: ```javascript // Get recommendations const result = await fetch('/api/recommend', {...}); const data = await result.json(); // Generate report URL const reportUrl = `/api/recommend/report?data=${btoa(JSON.stringify(data))}&lang=ko`; window.open(reportUrl); // Opens printable HTML ``` ### Region-Based Recommendation Strategy (`recommend.ts`) **When region IS specified** (e.g., `region_preference: ["seoul"]`): - Returns 3 spec tiers (Budget/Balanced/Premium) within that region - Example: Seoul 1 - Standard 4GB, Standard 8GB, Pro 16GB **When NO region specified**: - Returns same/similar spec from 3 DIFFERENT regions for location comparison - Example: Standard 4GB from Osaka 2, Seoul 1, Singapore 1 - Implemented by sending only 1 server per region to AI (forces diversity) ### AI Prompt Strategy (`recommend.ts`) - Uses OpenAI GPT-4o-mini via Cloudflare AI Gateway (bypasses regional restrictions) - Server list format: `[server_id=XXXX] Provider Name...` for accurate ID extraction - **server_id uses `ap.id`** (pricing ID, unique per instance+region combination) - Scoring: Cost efficiency (40%) + Capacity fit (30%) + Scalability (30%) - Capacity response in Korean for Korean users - **Prompt injection protection**: User inputs sanitized via `sanitizeForAIPrompt()` - **Token optimization**: Candidates limited to top 15 by price ### Security Features - **XSS prevention**: `escapeHtml()` applied to all user data in HTML reports - **Input validation**: Comprehensive checks with length limits (tech_stack ≤20, use_case ≤500, region_preference ≤10 items) - **Cache integrity**: Validates cached JSON structure before returning - **Rate limiting**: 60 req/min per IP with in-memory fallback (time-based sorting for cleanup) - **SQL injection prevention**: All queries use parameterized statements + `escapeLikePattern()` for LIKE queries - **Security headers**: CSP, HSTS, X-Frame-Options, X-Content-Type-Options - **Origin validation**: Restricts browser requests to `.kappa-d8e.workers.dev` domain only - **API key protection**: Keys never logged, sanitized from error messages - **Prompt injection protection**: `sanitizeForAIPrompt()` filters malicious patterns - **Password generation**: Rejection sampling for unbiased random character selection - **Type safety**: No `any` types - uses `unknown` + type guards - **Request size limits**: Base64 data parameter max 100KB for report endpoint - **Timeout protection**: AbortController with configurable timeouts for all external APIs ### AI Fallback System (`services/ai-service.ts`) When OpenAI API is unavailable, the system automatically falls back to rule-based recommendations: - Sorts candidates by price - Selects 3 tiers: Budget (cheapest), Balanced (median), Premium (top 25%) - Provides basic capacity estimates based on vCPU count - Warns user that AI is temporarily unavailable ### Server Provisioning API (`handlers/provision.ts`) Queue-based async server provisioning with automatic balance management. **Endpoints** (require `X-API-Key` header): | Endpoint | Method | Description | |----------|--------|-------------| | `/api/provision` | POST | Create server order (sends to Queue) | | `/api/provision/orders` | GET | List user's orders | | `/api/provision/orders/:id` | GET | Get order details (includes root_password) | | `/api/provision/orders/:id` | DELETE | Terminate server | | `/api/provision/balance` | GET | Get user balance (KRW) | **Provisioning Flow**: ``` POST /api/provision ↓ 1. Validate user (telegram_id) 2. Get pricing (anvil_pricing) 3. Calculate KRW price (exchange rate × 500원 rounding) 4. Deduct balance (atomic query) 5. Create order (status: provisioning) 6. Send to Queue → Return immediately ↓ [Queue Consumer] ↓ 7. Fetch order (get root_password from DB) 8. Call provider API (Linode/Vultr) 9. Wait for IP assignment (polling, max 2min) 10. Update order (status: active, ip_address) ↓ On failure: Refund balance + status: failed ``` **Request Body**: ```json { "user_id": "telegram_id", "pricing_id": 26, "label": "my-server", "image": "ubuntu_22_04", "dry_run": false } ``` **Security Features**: - API key authentication (`X-API-Key` header) - Origin validation for browser requests - Atomic balance deduction (prevents race condition) - Root password stored in DB only (not in Queue message) - Automatic refund on any failure **Supported Providers**: - Linode (`linode-provider.ts`) - Vultr (`vultr-provider.ts`) ### Admin SSH Key for Server Recovery Admin SSH key is automatically added to all provisioned servers for emergency recovery access. **Environment Variables** (set via `wrangler secret put`): ``` ADMIN_SSH_PUBLIC_KEY # Linode: public key string (ssh-rsa AAAA... or ssh-ed25519 AAAA...) ADMIN_SSH_KEY_ID_VULTR # Vultr: pre-registered SSH key ID (from Vultr dashboard/API) ``` **Provider Differences**: | Provider | Parameter | Value Type | How to Set | |----------|-----------|------------|------------| | Linode | `authorized_keys` | Public key string | Direct: `ssh-ed25519 AAAA...` | | Vultr | `sshkey_id` | Key ID | Register via dashboard/API first | **Vultr SSH Key Registration** (one-time setup): ```bash # Register key via Vultr API curl -X POST "https://api.vultr.com/v2/ssh-keys" \ -H "Authorization: Bearer $VULTR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"anvil-admin","ssh_key":"ssh-ed25519 AAAA..."}' # Response: {"ssh_key":{"id":"cb676a46-66fd-4dfb-..."}} # Use this ID for ADMIN_SSH_KEY_ID_VULTR ``` **Password Recovery Options**: | Provider | API Reset | Console Access | |----------|-----------|----------------| | Linode | ✅ `POST /linode/instances/{id}/password` | ✅ Lish via dashboard | | Vultr | ❌ Not available | ✅ View Console in dashboard | ### Configuration (`config.ts`) Centralized limits and constants: ```typescript LIMITS = { MAX_REQUEST_BODY_BYTES: 10240, // 10KB CACHE_TTL_SECONDS: 300, // 5 minutes RATE_LIMIT_MAX_REQUESTS: 60, // per minute MAX_AI_CANDIDATES: 15, // reduce API cost MAX_TECH_STACK: 20, MAX_USE_CASE_LENGTH: 500, MAX_REGION_PREFERENCE: 10, MAX_REGION_LENGTH: 50, MAX_BASE64_DATA_LENGTH: 100000, // ~75KB decoded } TIMEOUTS = { AI_REQUEST_MS: 30000, // 30 seconds for OpenAI VPS_PROVIDER_API_MS: 60000, // 60 seconds for Linode/Vultr TELEGRAM_API_MS: 10000, // 10 seconds for Telegram notifications } TECH_CATEGORY_WEIGHTS = { heavy_db: 0.3, // analytics, log server, reporting medium_heavy_db: 0.5, // e-commerce, ERP, CRM medium_db: 0.7, // API, SaaS, app backend light_db: 1.0, // blog, portfolio, wiki } ``` ## Bindings (wrangler.toml) ```toml [[kv_namespaces]] binding = "CACHE" id = "c68cdb477022424cbe4594f491390c8a" [[d1_databases]] binding = "DB" database_name = "cloud-instances-db" database_id = "bbcb472d-b25e-4e48-b6ea-112f9fffb4a8" [[d1_databases]] binding = "USER_DB" database_name = "telegram-conversations" database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e" [[queues.producers]] queue = "provision-queue" binding = "PROVISION_QUEUE" [[queues.consumers]] queue = "provision-queue" max_batch_size = 1 max_retries = 3 dead_letter_queue = "provision-queue-dlq" # Secrets (via wrangler secret put) # OPENAI_API_KEY - OpenAI API key for GPT-4o-mini # LINODE_API_KEY - Linode API token # VULTR_API_KEY - Vultr API key # PROVISION_API_KEY - API key for /api/provision/* endpoints # BOT_TOKEN - Telegram bot token for notifications (optional) # ADMIN_SSH_PUBLIC_KEY - Admin SSH public key for Linode (optional) # ADMIN_SSH_KEY_ID_VULTR - Admin SSH key ID for Vultr (optional) ``` ## Testing **Note**: Use single-line curl commands. Backslash line continuation (`\`) may not work in some environments. ```bash # Health check curl -s https://cloud-orchestrator.kappa-d8e.workers.dev/api/health | jq . # Recommendation - nodejs/redis real-time chat (Japan) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nodejs","redis"],"expected_users":1000,"use_case":"real-time chat","region_preference":["japan"]}' | jq . # Recommendation - php/mysql community forum (Korea) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["php","mysql"],"expected_users":800,"use_case":"community forum","region_preference":["korea"]}' | jq . # Recommendation - analytics dashboard (heavier DB workload) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["postgresql"],"expected_users":500,"use_case":"analytics dashboard","region_preference":["japan"]}' | jq . # Server list with filters (supports flexible region: korea, seoul, tokyo, etc.) curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/servers?region=korea&minCpu=4" | jq . # CDN cache hit rate - default (use_case별 자동 적용) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"]}' | jq '.bandwidth_estimate | {cdn_enabled, cdn_cache_hit_rate, gross_monthly_tb, origin_monthly_tb}' # CDN disabled (원본 서버 전체 트래픽) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"],"cdn_enabled":false}' | jq '.bandwidth_estimate' # Custom CDN cache hit rate (80%) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"],"cdn_cache_hit_rate":0.80}' | jq '.recommendations[0].bandwidth_info | {cdn_cache_hit_rate, cdn_savings_tb, cdn_savings_cost}' # HTML Report (encode recommendation result as base64) # 1. Get recommendation and save to variable RESULT=$(curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nodejs"],"expected_users":500,"use_case":"simple api","lang":"ko"}') # 2. Generate report URL with base64-encoded data REPORT_URL="https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend/report?data=$(echo $RESULT | base64 | tr -d '\n')&lang=ko" # 3. Open in browser or fetch echo $REPORT_URL # === Provisioning API (requires X-API-Key header) === # Dry run - validate without creating server curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision -H "Content-Type: application/json" -H "X-API-Key: $PROVISION_API_KEY" -d '{"user_id":"821596605","pricing_id":26,"label":"test","dry_run":true}' | jq . # Create server (async via Queue) curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision -H "Content-Type: application/json" -H "X-API-Key: $PROVISION_API_KEY" -d '{"user_id":"821596605","pricing_id":26,"label":"my-server"}' | jq . # Get user balance curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/balance?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . # List user orders curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/orders?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . # Get specific order (includes root_password) curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/orders/15?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . # Terminate server curl -s -X DELETE "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/orders/15?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . ``` ## Recent Changes ### Security Hardening & Admin SSH Key (Latest) **Security Improvements**: - CSP headers for HTML reports (`style-src 'unsafe-inline'`) - Origin validation restricts to `.kappa-d8e.workers.dev` domain - Base64 size limit (100KB) for report data parameter - Rejection sampling for unbiased password generation - SQL LIKE pattern escaping via `escapeLikePattern()` - Rate limiter cleanup with time-based sorting **Performance Improvements**: - Telegram API timeout (10s) with AbortController - Centralized TIMEOUTS config for all external APIs - VPS provider timeout configurable (default 60s) **New Features**: - Admin SSH key support for server recovery - `ADMIN_SSH_PUBLIC_KEY` for Linode (public key string) - `ADMIN_SSH_KEY_ID_VULTR` for Vultr (pre-registered key ID) - Middleware layer: auth, origin, rate-limit, request-id, security, user-id - Idempotency key for order deduplication **Code Quality**: - 404 status when no servers found (was 200 with empty array) - Consolidated error logging to single JSON.stringify - Import TECH_CATEGORY_WEIGHTS from config.ts **Files Added**: - `src/middleware/{auth,origin,rate-limit,request-id,security,user-id}.ts` - `migrations/004_add_idempotency_key.sql` ### CDN Cache Hit Rate **New Feature**: CDN 캐시 히트율 기반 원본 서버 트래픽 및 비용 계산 **API Parameters Added**: - `cdn_enabled`: CDN 사용 여부 (기본: true) - `cdn_cache_hit_rate`: 캐시 히트율 override (0.0-1.0) **Use Case별 기본 캐시 히트율**: - video/streaming: 92%, blog/static: 90%, file: 85% - ecommerce: 70%, forum: 60%, api: 30%, gaming: 20%, chat: 10% **Response Fields Added**: - `bandwidth_estimate`: cdn_enabled, cdn_cache_hit_rate, gross_monthly_tb, origin_monthly_tb - `bandwidth_info`: cdn_savings_tb, cdn_savings_cost **Cost Impact Example** (10K concurrent video streaming): - Without CDN: $18,370 overage - With CDN 92%: $1,464 overage (92% savings) **Tests Added**: 4 new CDN-specific tests (total 59 tests) ### Major Architecture Refactoring **Security Hardening**: - Fixed XSS vulnerability in HTML reports with `escapeHtml()` - Added cache data integrity validation - Added `region_preference` input validation (max 10 items, 50 chars each) - Replaced all `any` types with `unknown` + type guards **Architecture Improvements**: - Split `utils.ts` (801 lines) → 6 modular files in `utils/` - Extracted AI logic to `services/ai-service.ts` (recommend.ts -49%) - Added Repository pattern: `repositories/AnvilServerRepository.ts` - Reduced DB query duplication across handlers **New Features**: - AI fallback: Rule-based recommendations when OpenAI unavailable - Vitest testing: 55 tests covering validation, security, bandwidth - Duplicate server prevention in AI recommendations **Files Added**: - `src/utils/{index,http,validation,bandwidth,cache,ai,exchange-rate}.ts` - `src/services/ai-service.ts` - `src/repositories/AnvilServerRepository.ts` - `src/__tests__/{utils,bandwidth}.test.ts` - `vitest.config.ts` ### Spec Diversity & Bandwidth-Based Filtering (Latest) - **Spec diversity**: No region specified → 3 different spec tiers (Budget/Balanced/Premium) from different regions - **Bandwidth-based filtering**: High bandwidth workloads (>2TB) automatically prioritize servers with adequate transfer allowance - **Bandwidth warning**: `infrastructure_tips` includes warning when estimated traffic exceeds included bandwidth by 2x+ - **Previous issue fixed**: Was recommending same Basic 1GB spec × 3 (only region different), now recommends diverse specs ### Region Diversity & Bug Fixes - **Cache key fix**: `region_preference` now included in cache key - **Server ID fix**: Changed from `ai.id` (instance) to `ap.id` (pricing) for unique region+instance identification - **Prompt cleanup**: Removed obsolete Linode/Vultr/DigitalOcean references (Anvil only) ### Transfer Pricing & Reporting - **Transfer pricing**: Added `anvil_transfer_pricing` table data to recommendations - **bandwidth_info**: Each recommendation includes transfer costs (included_tb, overage costs) - **available_regions**: Lists other regions where same server spec is available with prices - **HTML report**: New `/api/recommend/report` endpoint for printable reports - **KRW conversion**: Bandwidth costs converted to KRW for Korean users (GB: 1원, TB/total: 100원 rounding) ### Anvil Pricing Migration - **New tables**: Migrated from `pricing` to `anvil_pricing` tables - **Provider**: Now uses "Anvil" as single provider (previously Linode/Vultr) - **Exchange rate**: Real-time USD→KRW conversion via open.er-api.com - **Removed**: `provider_filter` parameter no longer supported - **Currency handling**: Korean users see KRW, others see USD ### Architecture - **Modular architecture**: Split from single 2370-line file into organized modules - **Centralized config**: All magic numbers moved to `LIMITS` in config.ts - **Type safety**: `parseAIResponse(unknown)` with proper type guards ### Features - **DB workload multiplier**: Database resource calculation based on use_case - **Bandwidth estimation**: Automatic bandwidth category detection for provider filtering - **Tech specs update**: Realistic vcpu_per_users values for 150+ technologies ### Security - **Prompt injection protection**: `sanitizeForAIPrompt()` filters malicious patterns - **Rate limiting fallback**: In-memory Map when KV unavailable - **Input sanitization**: All user inputs validated and length-limited ### Performance - **O(1) VPS lookup**: Map-based benchmark matching (was O(n×m)) - **AI token optimization**: Candidates limited to 15 (was 50) - **KV caching**: 5-minute TTL, empty results not cached - **Parallel queries**: Promise.all for independent DB operations ### Code Quality - **Dead code removed**: Unused `queryVPSBenchmarks` function deleted - **Flexible region matching**: Both endpoints support country/city/code inputs (korea, seoul, icn)