# 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
├── 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
│   ├── bandwidth.ts      # Bandwidth estimation & cost calculation
│   ├── cache.ts          # Caching, rate limiting
│   ├── ai.ts             # AI prompt sanitization
│   └── exchange-rate.ts  # Currency conversion
├── 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
│   ├── 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
│   ├── 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 when KV unavailable
- **SQL injection prevention**: All queries use parameterized statements via Repository pattern
- **Security headers**: CSP, HSTS, X-Frame-Options, X-Content-Type-Options
- **API key protection**: Keys never logged, sanitized from error messages
- **Prompt injection protection**: `sanitizeForAIPrompt()` filters malicious patterns
- **Type safety**: No `any` types - uses `unknown` + type guards

### 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`)

### 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,
}
```

## 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, LINODE_API_KEY, VULTR_API_KEY, PROVISION_API_KEY
```

## 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

### CDN Cache Hit Rate (Latest)

**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)
