jim800121chen d8a9517c9d feat(task-scheduler): Phase 0.8b — API key auth + /result endpoint
Auth pillar 從 OAuth 2.0 resource server 改成 pre-shared API key
(visionA ↔ converter 1:1 internal trust)。新增 GET /api/v1/jobs/:id/result
streaming endpoint 給 visionA backend 中轉 NEF 下載。

Phase A(auth 切換):
- 新增 apiKeyMiddleware(constant-time compare、tokenFingerprint、4 audit events)
- 砍 OAuth middleware + JWKS(保留 oauthClient 供 promote → FAA 使用)
- 4 個 endpoint 換掛 requireApiKey
- 加 TRUST_PROXY env + Express trust proxy 設定(forensic source_ip)

Phase B(/result endpoint):
- streaming NEF download with 5min timeout + concurrent cap 10
- Two-tier rate limit(burst 5/10s + sustained 20/min)
- Bandwidth quota(1 GB/hr + 6 GB/24hr)by token_fingerprint
- Range header silently ignored + Accept-Ranges: none
- filename quote-escape + RFC 5987 fallback + sanitize
- 8 個 /result audit events(forensic 完整)

設計演進記錄:docs/TODO-visionA-integration-v2.md(5/2 OAuth → 5/16 API key
→ 5/16 download via converter;對應 visionA repo ADR-015/016)

Tests: 597 → 666 (+69)、29 suites all pass
Security: APPROVE WITH CONDITIONS(單 instance 部署、6 新 env、24hr 監控)
npm audit: 3 vuln → 0(transitive AWS SDK xml chain)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:47:28 +08:00

285 lines
13 KiB
Plaintext
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.

###############################################################################
# Task Scheduler 環境變數範本Phase 0.8b 收斂版)
#
# 三類分區(依顯示順序):
# 1. 必填production 必須設真實值)— 缺漏會 fail-fastprocess exit code 1
# 2. 可選(合理預設)— 不設會用程式內 default
# 3. 開發 placeholder — 用 RFC 2606 `.invalid` TLD 確保不會誤連到真實服務
#
# 部署準則:
# - 切勿 commit `.env`(已在 .gitignore歷史 commit 待 D7 處理)
# - production 用 secret managerVault / AWS Secrets Manager不要直接設環境變數
# - 任何含 `REPLACE-ME` 字樣或 `.invalid` TLD 的值,部署前必須替換
#
# Phase 0.8b A4 砍除visionA → converter 改用 API key、不再走 OAuth/JWKS
# - MEMBER_CENTER_ISSUER / MEMBER_CENTER_JWKS_URL
# - KNERON_CONVERTER_AUDIENCE / CONVERTER_TENANT_ID
# - CONVERTER_SCOPE_WRITE / CONVERTER_SCOPE_READ
# - JWKS_CACHE_MAX_AGE_MS / JWKS_COOLDOWN_MS / JWT_CLOCK_TOLERANCE_SEC
# converter → FAA OAuth client 鏈條保留不動MEMBER_CENTER_TOKEN_URL /
# KNERON_CONVERTER_CLIENT_* / FILE_ACCESS_AGENT_* / OAUTH_TOKEN_*),那條與
# visionA → converter 對外認證無關。
###############################################################################
# =============================================================================
# 1. 應用基本設定
# =============================================================================
# 監聽 port必填但有合理預設
PORT=4000
# Node 環境development / staging / production
# - production 時 FILE_ACCESS_AGENT_BASE_URL 強制 HTTPS
NODE_ENV=development
# Log 等級debug / info / warn / error
LOG_LEVEL=info
# =============================================================================
# 2. Redis必填
# =============================================================================
# - 不設會用 default但實際部署需指向真實 Redis
# - 帶 passwordredis://:password@host:6379
REDIS_URL=redis://localhost:6379
# =============================================================================
# 3. Job 資料目錄local storage 用)
# =============================================================================
# - STORAGE_BACKEND=local 時,此目錄為 worker / scheduler 共用 volume
# - STORAGE_BACKEND=minio 時,仍會用此目錄存暫時檔(如 health check
JOB_DATA_DIR=/data/jobs
# =============================================================================
# 4. CORS必填
# =============================================================================
FRONTEND_URL=http://localhost:3000
# =============================================================================
# 5. Storage backend必填
# =============================================================================
# - "local":用 JOB_DATA_DIR 共用 volume單機開發 / docker-compose
# - "minio":用 MinIO / S3-compatibleproduction 推薦POST /api/v1/jobs 必須 minio
STORAGE_BACKEND=local
# =============================================================================
# 6. MinIO / S3 設定
# =============================================================================
# - STORAGE_BACKEND=minio 時為必填
# - STORAGE_BACKEND=local 時可留空
# - 注意production 不要把真實 secret 寫在這裡,改用 secret manager
MINIO_ENDPOINT_URL=http://192.168.0.130:9000
MINIO_BUCKET=convertet-working-space
MINIO_ACCESS_KEY=convuser
MINIO_SECRET_KEY=REPLACE-ME-IN-PRODUCTION
MINIO_REGION=us-east-1
# bucket lifecycle— 上傳後 N 天自動清,避免 orphan 累積
MINIO_LIFECYCLE_DAYS=7
# =============================================================================
# 7. visionA → converter API Key 認證Phase 0.8b 必填)
# =============================================================================
#
# visionA-backend ↔ converter 為 1:1 internal trust採 pre-shared API key
# 取代 OAuth client_credentialsADR-015 v2.1。converter 端只需這一把 key
# 無 issuer / audience / scope / tenant 概念。
#
# 產生:
# $ openssl rand -hex 32
# # 輸出 64 hex chars128 bits 熵),對齊 NIST SP 800-131A
#
# 部署:
# - converter `.env`CONVERTER_API_KEY=<產出字串>
# - visionA `.env.stage`VISIONA_CONVERTER_API_KEY=<相同字串>
# - **兩端必須完全相同**rotate 時雙端同時更新
#
# 安全:
# - 絕不 commit 進 git`.env` 已在 .gitignore
# - 絕不寫入 log / Slack / email / 對話
# - 每個環境獨立 keydev / stage / prod 各自 `openssl rand -hex 32`
# - 詳見 docs/autoflow/04-architecture/auth.md §1 + §4
#
# 為什麼是 optional不 requireEnv
# - dev 環境可能還沒設 keylocal 跑 legacy Web UI 路徑也應該能啟動)
# - apiKeyMiddleware 自己會做 fail-fastenv 未設時對外 API 一律回 503
# - 啟動時會印 warn log 提醒,避免無聲問題
CONVERTER_API_KEY=
# =============================================================================
# 8. Member Center Token Endpointconverter → FAA OAuth client 用,保留)
# =============================================================================
#
# 給 oauthClient.js 取 service token 用promote 流程把結果檔 PUT 到 FAA 之前要
# 先用 client_credentials grant 取 token。visionA → converter 路線**不再經此**。
#
# ⚠️ `.invalid` 為 RFC 2606 保留 TLDDNS 永不解析。本地開發跑「不需 promote 的
# legacy /jobs 流程」可直接照抄production 部署前務必替換為真實 Member Center
# URL否則 promote 階段取 token 會 DNS 失敗。
MEMBER_CENTER_TOKEN_URL=https://auth.example.invalid/oauth/token
# =============================================================================
# 9. Converter OAuth Client 身份converter → FAA 用,保留)
# =============================================================================
#
# Converter 在 promote 階段以 OAuth client 身份去 Member Center 取
# `files:upload.write` scope token再 PUT 到 File Access Agent。
# 兩者必須成對出現。
KNERON_CONVERTER_CLIENT_ID=kneron_converter_dev
KNERON_CONVERTER_CLIENT_SECRET=REPLACE-ME-IN-PRODUCTION
# =============================================================================
# 10. File Access Agent必填
# =============================================================================
#
# Promote 時 Converter 把產出 stream PUT 到 FAA。
# - URL 必須是合法 http(s) URLNODE_ENV=production 強制 https
# - 本地開發可用 placeholder.invalid TLD不影響非 promote 流程
FILE_ACCESS_AGENT_BASE_URL=https://files.example.invalid
FILE_ACCESS_AGENT_AUDIENCE=file_access_api
# =============================================================================
# 11. Promote 行為(可選)
# =============================================================================
# 單檔 PUT timeout毫秒。預設 300000300s覆蓋 500MB @ 5MB/s 最壞)。
# 部署環境檔案普遍較小可調低GB 級檔案可調高。
# PROMOTE_TIMEOUT_MS=300000
# =============================================================================
# 12. OAuth Client cacheconverter → FAA 用,可選)
# =============================================================================
# OAUTH_TOKEN_REFRESH_SKEW_MS=60000 # token 距 expiresAt 還剩多少 ms 主動 refresh
# OAUTH_TOKEN_TIMEOUT_MS=10000 # 取 token timeout10s
# =============================================================================
# 13. Multipart 上傳上限可選T10 修 D5
# =============================================================================
#
# 為什麼用 env
# 不同部署環境記憶體配額差異大dev 容器 2GB / prod 16GB固定 500MB 不夠彈性。
# 調這些值不需改原始碼。
#
# 三個值都必須 > 0非法值會 fail-fast。
# MULTIPART_MODEL_MAX_BYTES=524288000 # 500MBmodel 檔案上限)
# MULTIPART_REF_IMAGE_MAX_BYTES=10485760 # 10MB單張 ref_image 上限)
# MULTIPART_REF_IMAGES_MAX_COUNT=100 # ref_images 張數上限
# =============================================================================
# 14. Upload concurrency可選T10 修 D5
# =============================================================================
#
# 為什麼需要:
# multer memoryStorage 把整份 multipart load 進 buffer每個並發 upload 吃掉
# model size 大小的 heap。5 並發 × 500MB ≈ 2.5GB heap4GB 容器有風險。
# per-process counter 限制同時間 multipart parse + handler 進行中的請求數量。
#
# 超過上限時:直接 503 service_busy + Retry-After header不 queue讓 client 主動 backoff。
# MAX_CONCURRENT_UPLOADS=5 # 同時間最多 5 個 upload 進行中
# UPLOAD_RETRY_AFTER_SECONDS=30 # 503 response 的 Retry-After
# =============================================================================
# 15. Per-client_id rate limit可選T3 起)
# =============================================================================
# 對 /api/v1/* 套用window 內每個 client_id 最多 max 個 request。
# 預設 5min / 300 req對齊 TDD §1.1)。
#
# Phase 0.8b A3 後clientId 在 API key 路線下寫死為 `'visionA-service'`
# rate limit 仍然套用、實質為 visionA → converter 整體上限。
# API_V1_RATE_LIMIT_WINDOW_MS=300000
# API_V1_RATE_LIMIT_MAX=300
# =============================================================================
# 16. Trust Proxy可選Phase 0.8b A7
# =============================================================================
# 給 Express `app.set('trust proxy', ...)` 用,影響 `req.ip` 取得真實 caller IP。
# 直接影響 audit log 中 `source_ip` 欄位的 forensic 價值。
#
# 為什麼需要:
# converter 跑在 Nginx / cloud LB 後面時Node 看到的 remote address 永遠是
# 反代理的內網 IP必須信任 `X-Forwarded-For` header 才能取真實 caller IP。
#
# ⚠️ 安全提醒(極度重要):
# - 設過寬(如 `true`)→ attacker 可偽造 `X-Forwarded-For` 欺騙 audit log
# - 設過嚴(如 stage / prod 留 'loopback')→ source_ip 永遠是反代理 IP、forensic 失效
# - 必須跟實際部署架構一致;不確定時請問 DevOps / SRE
#
# 部署建議:
# - local dev / 測試環境:留空(預設 `loopback`,只信任 127.0.0.1 / ::1
# - stage / prod前面 1 層 NginxTRUST_PROXY=1
# - stage / prodcloud LB + Nginx 兩層TRUST_PROXY=2
# - 明確 CIDRTRUST_PROXY="loopback, 10.0.0.0/8"
#
# 接受值boolean`true` / `false`/ 整數 / 字串(含 'loopback' / 'linklocal'
# / 'uniquelocal' / CIDR。詳見 Express 文件:
# https://expressjs.com/en/guide/behind-proxies.html
# TRUST_PROXY=1
# =============================================================================
# 17. GET /api/v1/jobs/:id/resultPhase 0.8b Phase B 新增)
# =============================================================================
#
# `/result` 端點給 visionA-backend streaming proxy NEF binary。所有限制
# 都以 `token_fingerprint`sha256(api_key).slice(0,12))為 bucket key。
#
# 設計docs/autoflow/04-architecture/api/api-result.md §9 / §15
#
# 為什麼 single instance 部署可接受 in-memory counter
# - Phase 0.8b 部署是單 Node processcounter 在 process 內 atomic
# - Phase 2 多 instance 部署前必切 Redis否則 limit 會被「乘以 instance 數」
# 放鬆);見 security.md 候補 #8HIGH
# Stream response timeoutAC-7— 預設 5 分鐘300_000 ms
# 5 min 最低 throughput ≈ 1.7 MB/s合法 client 即使中等網路也能拿完 500MB
# RESULT_STREAM_TIMEOUT_MS=300000
# Concurrent stream capAC-4— 預設 10 個同時 streamper-instance
# 超過 → 503 service_busy + Retry-After: 30
# 平衡normal load P95 < 5、留 2× headroomblast radius 可控
# MAX_CONCURRENT_RESULT_STREAMS=10
# Burst rate limitAC-2— 預設 5 req / 10 secper token_fingerprint
# 阻擋短時間 burst 攻擊;允許 visionA retry pattern
# RESULT_RATE_LIMIT_BURST_PER_10S=5
# RESULT_RATE_LIMIT_BURST_WINDOW_MS=10000
# Sustained rate limitAC-2— 預設 20 req / 1 minper token_fingerprint
# 涵蓋 visionA P95 normal load + 1.7× headroom阻擋持續 mass request
# RESULT_RATE_LIMIT_SUSTAINED_PER_MIN=20
# RESULT_RATE_LIMIT_SUSTAINED_WINDOW_MS=60000
# Bandwidth quotaAC-3— 預設 1 GB / hr + 6 GB / 24hrper token_fingerprint
# 阻擋 attacker 用「剛好踩線 req count」配大檔的 bandwidth abuse
# 1 GB = 1073741824 / 6 GB = 6442450944
# RESULT_BANDWIDTH_QUOTA_PER_HOUR_BYTES=1073741824
# RESULT_BANDWIDTH_QUOTA_PER_DAY_BYTES=6442450944