Files
cloud-orchestrator/CLAUDE.md
kappa f62712af37 docs: update CLAUDE.md with security hardening and admin SSH key
- Add middleware directory to architecture diagram
- Document Admin SSH Key for server recovery
- Update Security Features section (origin validation, timeouts, etc.)
- Add TIMEOUTS and TECH_CATEGORY_WEIGHTS to config section
- Update secrets list with SSH key variables
- Add latest changes section for security hardening

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:39:26 +09:00

628 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)