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>
29 KiB
Design Doc — Kneron Model Converter 對外 API
作者:Architect Agent
狀態:Draft(Phase 0.8b 重寫,三方交叉審閱前)
最後更新:2026-05-16
範圍:Phase 1 完工 + Phase 0.8b 設計轉向(API key + /result endpoint)
auth 設計演進:本文件反映 Phase 0.8b 拍板後的「目標狀態」(API key +
/result中轉)。完整決策歷史見 visionA repo:
docs/autoflow/04-architecture/adr/adr-015-server-to-server-api-key.mdv2.1 — 為什麼 visionA → converter 改用 pre-shared API keydocs/autoflow/04-architecture/adr/adr-016-download-via-converter.mdv1.0 — 為什麼 download 改成 converter/result中轉- converter 端的設計沿革見本 repo
git log docs/autoflow/04-architecture/design-doc.md
變更歷程
| 日期 | 變更 | 作者 |
|---|---|---|
| 2026-04-25 | 初版 Draft;OAuth resource server + promote 設計 | Architect Agent |
| 2026-04-25 | 原始模型上傳路徑改 multipart 直傳;移除 FAA GET/HEAD 相關 | Architect Agent |
| 2026-05-16 | Phase 0.8b 重寫:visionA → converter 改 API key;新增 GET /api/v1/jobs/:id/result streaming endpoint;保留 converter → FAA OAuth client(promote 用) |
Architect Agent |
0. 文件導讀
本 Design Doc 聚焦「系統層級架構決策」。工程師實作細節請看 TDD.md 索引及其子檔案。
對應文件:
- 產品需求:
../02-prd/PRD.md - 使用者流程:
../03-design/user-flow-cross-system.md - 設計審閱:
../03-design/design-review.md - 專案健檢:
.autoflow/00-onboarding/health-check.md - 安全設計:
security.md - visionA repo 的 ADR-015 / ADR-016(caller 端設計脈絡)
1. 背景與範圍 (Context and Scope)
1.1 背景
Kneron Model Converter(下稱 Converter)原本是「只有 Web UI 的內部工具」,支援 AI 工程師以 GUI 執行 ONNX → BIE → NEF 三階段模型轉檔。L 級新功能將其擴展為「對外提供 REST API 的服務」,讓 Innovedus 生態中的其他服務(首個消費者為 visionA-backend)能以程式化方式整合轉檔能力。
Phase 1 上線後(2026-04-25 完工、5/9 部署 stage)撞了兩個 root cause、5/16 拍板設計轉向:
- 5/9 stage e2e blocker:MC 沒註冊
converter:job.read/writescope、converter image 過舊、FAA OAuth 狀態不明 → ADR-015 拍板 visionA ↔ converter 為 1:1 internal trust、改用 pre-shared API key - 5/16 grep MC source:發現 MC 從未實作
/file-access/download-tokensendpoint、delegated download token 鏈從 5/2 寫完到現在一直是斷的 → ADR-016 拍板改成 visionA → converterGET /api/v1/jobs/:id/result中轉
1.2 生態組成
- Converter(本專案):Node.js Task Scheduler + Python Worker,部署在靠近 NAS 網段
- visionA-backend:Go 服務,Converter 對外 API 的唯一 caller(Persona C)
- Member Center(MC):OAuth2 / OIDC authorization server(C#)— Phase 0.8b 後 Converter 對外 API 不再經過 MC;但 Converter → FAA 仍以 client_credentials 取 token,這條保留
- File Access Agent(FAA):tenant 邊界內檔案閘道(C# / ASP.NET Core),駐守 NAS 側,single-tenant per instance
1.3 系統定位
flowchart TB
subgraph AWS["AWS 側"]
VisionAFE["visionA 前端"]
VisionABE["visionA-backend<br/>(Go)"]
MC["Member Center<br/>(只給 converter → FAA<br/>取 promote token)"]
end
subgraph NAS["NAS 側(內部網段)"]
subgraph ConverterNode["Converter 部署節點"]
Nginx["Nginx<br/>(public + internal vhost)"]
Scheduler["Task Scheduler<br/>(Node.js Express)"]
Redis["Redis<br/>(job state + user index)"]
Workers["Workers<br/>(onnx / bie / nef)"]
ConvBucket["Converter Bucket<br/>(MinIO, 7d lifecycle)"]
end
FAA["File Access Agent<br/>(C# ASP.NET Core)"]
NasBucket["NAS Bucket<br/>(模型庫長期儲存)"]
FAA --- NasBucket
end
VisionAFE -->|HTTPS| VisionABE
VisionABE -->|1. POST /api/v1/jobs<br/>multipart<br/>Authorization: Bearer<br/><API key>| Nginx
Nginx -->|public vhost| Scheduler
Scheduler -->|constant-time<br/>compare API key| Scheduler
Scheduler -->|寫 input| ConvBucket
Scheduler -->|put / get job state| Redis
Workers -->|consume queue| Redis
Workers -->|read input / write output| ConvBucket
VisionABE -->|2. GET /api/v1/jobs/:id<br/>(poll, Bearer API key)| Nginx
VisionABE -->|3. POST /api/v1/jobs/:id/promote<br/>(Bearer API key)| Nginx
Scheduler -->|3a. 取 token<br/>client_credentials| MC
Scheduler -->|3b. PUT 結果檔<br/>(files:upload.write)| FAA
VisionABE -->|4. GET /api/v1/jobs/:id/result<br/>(Bearer API key)<br/>NEW Phase 0.8b| Nginx
Scheduler -->|4a. stream 從 MinIO| ConvBucket
ConvBucket -->|NEF binary stream| Scheduler
Scheduler -->|stream proxy<br/>application/octet-stream| VisionABE
classDef aws fill:#ffe0b2,stroke:#ef6c00
classDef nas fill:#c8e6c9,stroke:#2e7d32
classDef new fill:#bbdefb,stroke:#1565c0
class AWS aws
class NAS nas
class Nginx,Scheduler new
關鍵資料流(Phase 0.8b 後):
- 建 job(visionA-backend → Converter,multipart/form-data)
POST /api/v1/jobs帶Authorization: Bearer <CONVERTER_API_KEY>。Converter middleware 用crypto.timingSafeEqualconstant-time compare API key,通過後把 multipart 寫入 Converter Bucket(jobs/{job_id}/input/{filename})。 - 轉檔(Worker pool 順序處理 onnx → bie → nef) Workers 從 Converter Bucket 讀寫,產出結果寫回 Converter Bucket。
- Polling(visionA-backend → Converter)
每 2-5 秒
GET /api/v1/jobs/:id。 - Promote 到 NAS(visionA-backend → Converter → MC → FAA)
POST /api/v1/jobs/:id/promote時 Converter 用既有 OAuth client(cached)取files:upload.writetoken,PUT 結果檔到 FAA。這條 OAuth 鏈條保留不變。 - Download(visionA-backend → Converter,Phase 0.8b 新增)
GET /api/v1/jobs/:id/resultstreaming 從 Converter Bucket 把 NEF binary proxy 回 visionA-backend。不經過 FAA、不經過 MC。
2. 目標與非目標 (Goals and Non-Goals)
Goals(Phase 0.8b 後)
- 對外 API(visionA → converter)用 pre-shared API key 驗證
- Converter 仍保留 OAuth Client 身分(promote 流程用 client_credentials 取 FAA token)
- 提供 5 個對外端點:
POST /api/v1/jobs、GET /api/v1/jobs、GET /api/v1/jobs/:id、POST /api/v1/jobs/:id/promote、GET /api/v1/jobs/:id/result(新增) /resultendpoint streaming proxy NEF binary(不 buffer,支援數百 MB)- 同使用者同時一個轉檔限制(以
user_id為界) - Recovery 支援(
GET /api/v1/jobs?user_id=...&status=in_progress) - 既有
/jobs/*舊路徑保留不動,Web UI 零影響 - 部署分流:公網只開
/api/v1/*,/jobs/*只在內網可達 - OpenAPI 3.0 規格產出
- API SLA 可觀測(p95、錯誤率)
Non-Goals(Phase 0.8b 明確不做)
- 不再做 OAuth resource server(visionA → converter 不驗 MC JWT;只有未來真有第二個 caller 才考慮回補)
- 不做 delegated download token(MC 沒實作對應 endpoint、ADR-016 改成
/result中轉) - Webhook / SSE 對外推送(polling +
/result已足夠) - Job 取消 / 重試(非本次範圍)
- Job 持久化 / 跨 Crash recovery(維持「Crash 即 Reset」哲學)
- Web UI 改走 API key 流程(內網工具,不動)
- 使用者層級 ACL(責任邊界在 visionA-backend)
- 多租戶(single-tenant per Converter deployment)
3. 架構設計 (The Actual Design)
3.1 架構模式選擇
- 選擇:維持現有 單體 Task Scheduler(Node.js Express)+ Worker Pool(Python) 架構
- 理由:
- Phase 0.8b 改動範圍是「換 auth middleware + 加一個 streaming endpoint」,不足以撐起新服務的運維複雜度
- 既有 Crash 即 Reset 哲學對單體有利:Scheduler stateless,重啟 = 復原
- 取捨:
- Scheduler 單體承擔 download proxy 的網路 I/O;NEF 通常 < 50MB,stream 模式下記憶體足跡受 Node fetch / HTTP buffer 控制(不會 buffer 整個檔)
- 若未來 download QPS 高,可在 Nginx 層加 sendfile / proxy_cache,或把
/result拆出獨立微服務(Phase 1 不做)
3.2 Auth 策略:純 API key(1:1 internal trust)
為什麼從 OAuth 改 API key
| 維度 | OAuth client_credentials | Pre-shared API key |
|---|---|---|
| 跨團隊依賴 | 需要 MC 註冊 audience / client / scope | 雙方協議好 secret 即可 |
| 部署阻塞 | 5/9 stage 撞到 MC 沒註冊 scope | 無 |
| trust model | 多 caller、scope-based authorization | 1:1 internal trust,full access |
| code 複雜度 | JWKS cache + JWT verify + scope check | constant-time string compare |
| Token rotation | MC 端管理 | 雙方手動同步 .env |
| 適用情境 | 多 caller、跨組織 | 單一 caller、同組織 |
當前 visionA ↔ converter 是 1:1 internal trust(同公司、單一 caller),OAuth 是 over-engineering。
API key middleware 設計
- 接受:
Authorization: Bearer <CONVERTER_API_KEY>(重用既有 Bearer header 格式,client / log infra 不變) - 比對:
crypto.timingSafeEqualconstant-time compare(防 timing attack) - 失敗行為:401
invalid_token、回應後主動socket.destroy()(沿用既有 OAuth middleware 的 M2 行為,防大檔 body 繼續灌入) - req.auth shape:通過後設定
req.auth = { sub: 'visionA-service', clientId: 'visionA-service', tenantId: null, scopes: ['converter:job.write', 'converter:job.read'], raw: { authType: 'api_key' } },下游 handler / rate limiter / log 不需大改 - Fail-fast:啟動時
CONVERTER_API_KEY未設定 → 直接 503 拒絕所有 request(不 silently allow)
詳細實作見 auth.md §1。
Trust boundary 簡化
| 風險 | OAuth 模型 | API key 模型 |
|---|---|---|
| caller 被 compromise → 冒充任意 user_id | 需要 OBO / HMAC-signed user_id 緩解 | 同樣風險、但 API key 本身就是 visionA 服務的完整身分證明、不需要 OBO |
| Token / Key rotation | MC 介面管理 | 手動 rotate 雙端 .env + redeploy |
→ API key 沒有比 OAuth 更安全(trust boundary 模型一致),但也沒有更不安全。差別只在 rotation 操作複雜度。
詳見 security.md。
3.3 /result endpoint:streaming proxy
為什麼需要
ADR-016 §1:MC 沒實作 POST /file-access/download-tokens、FAA 的 MemberCenterDelegatedDownloadTokenValidator 從來沒跑通。delegated download token 鏈在 Phase 1 設計時就是斷的、只是因為從未 e2e 過所以沒人發現。
→ visionA 直接 download FAA 不可行(少了 delegated token endpoint)。改設計成 visionA → converter 拿 NEF。
架構位置
visionA-backend Converter Scheduler MinIO
(Converter Bucket)
GET /jobs/:id/result
Authorization: Bearer
<CONVERTER_API_KEY>
────────────────────→
requireApiKey middleware
────────────────────────
getJob from Redis
────────────────────────
check status / expires_at
────────────────────────
extractNefObjectKey
────────────────────────
minio.getObjectStream(nefKey)
─────────────────────────────────→
stream
←─────────────────────────────────
set headers:
Content-Type: application/octet-stream
Content-Length: ...
Content-Disposition: attachment;
filename="<stem>_<chip>.nef"
pipe stream → response
←──────────────────────── (streaming NEF binary)
關鍵設計決定
- 不 buffer 整個檔:用 Node stream
pipe(res),NEF 可能數百 MB - Content-Length 必須帶:visionA 端用來決定 timeout
- Filename 規則:
<source_filename_stem>_<chip>.nef(例:yolov5s.onnx+KL720→yolov5s_kl720.nef);fallbackjob_<jobID>.nef - 雙路徑 NEF key 解析:支援新格式(
result_object_keys.nef)+ 舊格式(output.nef_path),對齊 promote 流程的getJobOutputKey邏輯 - 4xx 情境:401(invalid API key)/ 404(job_not_found)/ 409(job_not_completed)/ 410(result_expired,過 7 天)/ 502(storage_unavailable)/ 503(service_unavailable)
- Stream error handling:headers 送出後 stream 失敗 → 只能
res.destroy()、client 看到 ECONNRESET;clientreq.on('close')→ 主動釋放 MinIO stream connection
詳細 API spec 見 api/api-result.md。
3.4 Phase 0.8b 改動範圍總覽
| 元件 | 改動 |
|---|---|
| Nginx | 不動(既有 public vhost 已 proxy /api/v1/*) |
| Task Scheduler — auth | 大改:新增 apiKeyMiddleware、移除 auth/middleware.js (OAuth) / auth/jwks.js(但保留 auth/oauthClient.js,promote 用) |
| Task Scheduler — routes | 小改:POST /jobs / GET /jobs / GET /jobs/:id / POST /jobs/:id/promote 改掛 requireApiKey();新增 /jobs/:id/result 路由 + handler |
| Task Scheduler — config | 移除 MEMBER_CENTER_ISSUER / MEMBER_CENTER_JWKS_URL / KNERON_CONVERTER_AUDIENCE / JWKS_*;保留 MEMBER_CENTER_TOKEN_URL / KNERON_CONVERTER_CLIENT_ID / KNERON_CONVERTER_CLIENT_SECRET / FILE_ACCESS_AGENT_*(promote 用);新增 CONVERTER_API_KEY |
| Redis 資料模型 | 不動 |
| Workers | 不動 |
| MinIO(Converter Bucket) | 不動 |
| FAA / MC | 不動(converter → FAA 仍走 OAuth client_credentials) |
3.5 技術選型(Technology Radar)
| 層級 | 技術選擇 | 狀態 | 選型理由 | 退出成本 |
|---|---|---|---|---|
| Auth — API key compare | crypto.timingSafeEqual |
Adopt | Node 標準庫,constant-time | 低 |
| Auth — 取 FAA token(promote) | 自寫 OAuth client + Basic auth + in-memory cache | Adopt | 既有實作、Phase 1 已驗證 | 低 |
| HTTP client 對 FAA(promote) | Node 18 原生 fetch + stream | Adopt | 支援大檔 stream | 低 |
Stream proxy(/result) |
Node Stream pipe(res) |
Adopt | 標準做法,記憶體足跡受控 | 低 |
| Rate Limit | express-rate-limit(既有)+ per-client_id key |
Adopt | 既有套件 | 低 |
| OpenAPI 產出 | 手寫 YAML | Adopt | 規格穩定、人工可審 | 低 |
| Redis 索引 | Redis Set(user:{user_id}:jobs)+ job:{id} |
Adopt | 不引入 PG | 中 |
3.6 API 設計概覽
- API 風格:REST + JSON(
/result例外,回傳 binary stream) - Base Path:
/api/v1/* - 認證:所有端點(除
/health)都要Authorization: Bearer <CONVERTER_API_KEY> - 錯誤格式:統一
{error: {code, message, details, request_id}} - 版本策略:breaking change 走
/api/v2/*,小變更在/api/v1/*內向後相容新增欄位 - Rate Limit:以
client_id(API key 模式下固定visionA-service)為 key,預設 300 req / 5min - ETag 支援:
GET /api/v1/jobs/:id支援If-None-Match,304 Not Modified 省流量
詳細 API spec 見 TDD.md 索引 + api/*.md 子檔案。
3.7 資料架構
核心資料(Redis)
| Key | 類型 | 內容 | TTL |
|---|---|---|---|
job:{job_id} |
String (JSON) | Job 完整 record(含 user_id、tenant_id、created_by_client_id、metadata、stage_timings、expires_at、result_object_keys、promoted_object_keys) |
7 天 |
user:{user_id}:jobs |
Set | 該 user 的 job_id 集合 | 隨最新 job 延長,建議 7 天 |
user:{user_id}:active_job |
String | 當前 in-progress job_id | 隨 job 完成時刪除 |
ratelimit:client:{client_id} |
由 express-rate-limit 管理 |
— | 5 min |
不引入 PostgreSQL:Phase 1 資料量小(單 user 7 天內 < 10 個 job),Redis 足以承擔。
資料流(Phase 0.8b)
flowchart LR
BE[visionA-backend] -->|1. POST /api/v1/jobs<br/>multipart + Bearer API key| Sched[Scheduler]
Sched -->|2. constant-time<br/>compare API key| Sched
Sched -->|3. 檢查 active_job| Redis[(Redis)]
Sched -->|4. multer 寫 input| CB[(Converter Bucket)]
Sched -->|5. 建 job + 索引| Redis
Sched -->|6. enqueue onnx| Redis
Workers[Workers] -->|consume| Redis
Workers -->|read/write| CB
Workers -->|done event| Redis
BE -->|7. poll GET /api/v1/jobs/:id| Sched
Sched -->|8. 讀 job| Redis
BE -->|9. POST /promote| Sched
Sched -->|10. 取 FAA token<br/>(client_credentials, cache)| MC[Member Center]
Sched -->|11. 讀結果| CB
Sched -->|12. PUT 結果| FAA[File Access Agent]
BE -->|13. GET /jobs/:id/result<br/>NEW Phase 0.8b| Sched
Sched -->|14. stream NEF| CB
CB -->|15. NEF binary| Sched
Sched -->|16. stream proxy| BE
4. 可靠性設計 (Reliability)
4.1 SLI / SLO
| 服務 | SLI | SLO | 依據 |
|---|---|---|---|
/api/v1/* 可用率 |
2xx+3xx 請求 / 總請求 | ≥ 99.5%(工作時段) | PRD §9.2.1 |
GET /api/v1/jobs/:id p95 |
回應時間 95 百分位 | < 200ms | PRD §9.2.1 |
POST /api/v1/jobs p95 |
multipart 上傳到 MinIO 完成 | < 5s(200MB)/ < 12s(500MB) | 詳見 performance.md |
POST /api/v1/jobs/:id/promote p95 |
回應時間 95 百分位 | < 3s | PRD §9.2.1 |
GET /api/v1/jobs/:id/result p95 |
TTFB(time to first byte) | < 500ms | Phase 0.8b 新增;NEF 50MB stream 在 50MB/s 鏈路下完整下載 ≈ 1s |
| API key 驗證失敗率 | 401 / 總請求 | < 0.1%(同 caller 不該錯) | Phase 0.8b 新增 |
4.2 容錯設計
| 失敗情境 | 設計應對 |
|---|---|
| MC token endpoint 不可達(取 Converter 自己的 FAA token) | Token cache(有效期內不重取);過期後重試 3 次,失敗則 promote 回 503 + auth_service_unavailable |
| multipart 上傳失敗 / 超過 500MB | POST /api/v1/jobs 回 400 / 413 |
| FAA promote 失敗 | Converter Bucket 檔案保留 7 天,可重試 promote |
/result MinIO stream 失敗(過期清除) |
410 result_expired |
| Worker Crash | 既有 Crash 即 Reset;Worker 重啟後繼續 consume Redis Stream |
| Redis Crash | 符合「Crash 即 Reset」設計哲學,所有 job 遺失 |
| Scheduler Crash | 重啟後繼續服務 |
4.3 災難復原
維持既有 Crash 即 Reset:RPO = 無保證、RTO < 30s(Docker restart)。
5. 安全架構 (Security)
詳見 security.md。本節僅列高層原則。
5.1 威脅模型(STRIDE 摘要,Phase 0.8b 版)
| 威脅 | 風險 | 防護 |
|---|---|---|
| Spoofing(偽造 visionA-backend) | 中 | API key constant-time compare;TLS protected 傳輸 |
| Spoofing(偽造 user_id) | 中(接受) | 信任 visionA-backend 的 user_id;Converter 不做 user ACL |
| Tampering(改 job record) | 低 | Redis 在內部網段 |
| Repudiation(否認呼叫) | 中 | Log client_id + request_id + user_id,保留 30 天 |
| Info Disclosure(跨 client 看別人的 job) | 低(Phase 0.8b 只有 1 個 caller) | Job 查詢預設過濾 created_by_client_id |
| DoS | 中 | Rate Limit per client_id;檔案大小上限 500MB |
| Elevation of Privilege | 降低(API key 沒有 scope,但只有 1 個 caller) | API key 即為完整 caller 身分 |
5.2 API key 管理
- 產生:
openssl rand -hex 32(64 hex chars) - 部署:放 .env / docker-compose env / k8s secret
- 雙端對齊:visionA
VISIONA_CONVERTER_API_KEY= converterCONVERTER_API_KEY - Rotation 策略:每環境獨立(dev / stage / prod);外洩時雙端同步 rotate + redeploy
- 絕不進 git / Slack / email / log(log 只記
api_key_length或api_key_set: trueboolean)
5.3 部署分流
維持既有設計:Nginx 雙 vhost(public /api/v1/* + internal /jobs/*)。詳見 infra.md。
6. 效能工程 (Performance)
詳見 performance.md。重點:
POST /api/v1/jobsp95 < 5s(200MB) / < 12s(500MB)GET /api/v1/jobs/:idp95 < 200msGET /api/v1/jobs/:id/resultTTFB < 500ms(Phase 0.8b 新增 SLO)POST /api/v1/jobs/:id/promotep95 < 3s
7. 部署架構
詳見 infra.md。重點:
- Nginx 雙 vhost(
/api/v1/*public、/jobs/*internal)— 不動 - 環境變數變動:移除
MEMBER_CENTER_ISSUER/JWKS_URL/KNERON_CONVERTER_AUDIENCE/JWKS_*;新增CONVERTER_API_KEY;保留MEMBER_CENTER_TOKEN_URL/KNERON_CONVERTER_CLIENT_*/FILE_ACCESS_AGENT_* - 部署順序:converter 先 deploy(並存舊 OAuth + 新 API key 一段時間)→ verify
/result→ visionA deploy → e2e → 砍 OAuth 殘留
8. ADR(架構決策紀錄)
ADR-001:對外 API 採 Member Center OAuth2(已 superseded)
狀態:Superseded by ADR-010(Phase 0.8b)
原因:5/9 stage e2e 撞 MC 沒註冊 scope;visionA ↔ converter 為 1:1 internal trust、OAuth 為 over-engineering。
→ visionA repo adr-015-server-to-server-api-key.md v2.1 記錄完整轉向理由。
ADR-002:promote 結果檔採「做法 2」— Converter 自己推到 FAA
狀態:Accepted(不變,Phase 0.8b 保留)
範圍:本 ADR 只針對結果檔 promote 的搬檔路徑;原始模型不會進 FAA。Converter → FAA 仍走 OAuth client_credentials + files:upload.write scope。
詳細決策內容沿用前版(5/2 寫入)。
ADR-003:user_id 以 multipart 欄位傳遞
狀態:Accepted(不變,Phase 0.8b 保留)
API key 模式下,user_id 仍是 visionA-backend 傳的 multipart field。Trust boundary 假設不變(visionA-backend 內部受控)。
ADR-004:Polling 而非 Webhook
狀態:Accepted(不變)
ADR-005:Phase 1 使用者下載改用 /result 中轉(取代原 delegated download token 設計)
狀態:Accepted(Phase 0.8b 拍板,取代原 ADR-005「Phase 1 使用者下載延至 Phase 2」)
背景:5/16 grep MC source 發現 MC 從未實作 /file-access/download-tokens endpoint;FAA 的 MemberCenterDelegatedDownloadTokenValidator.cs 假設 MC 有對應 introspection endpoint,也是假設錯了。delegated download token 鏈從 5/2 寫完到現在一直是斷的。
決定:不動 MC、不動 FAA。改設計成 visionA → converter GET /api/v1/jobs/:id/result 中轉。
理由:
- MC owner 時程不可控,延 Phase 2 不可行
- NEF 通常 < 50MB,streaming proxy 對 Converter Scheduler 不重
- 走 converter 而非 FAA,與
/promote同一個 caller 介面,visionA 端 client 邏輯統一
代價:
- Converter Scheduler 多扛一條 download 路徑(streaming,記憶體足跡受控)
- NEF 在 Converter Bucket 7 天 TTL 後過期,client 需處理 410
result_expired - 雙存(promote 後 NEF 仍在 Converter Bucket 7 天 + FAA NAS Bucket 永久),下載走 Converter Bucket、不下載 FAA
替代方案:詳見 visionA repo adr-016-download-via-converter.md v1.0 §3(6 個方案完整分析)。
ADR-006:Phase 1 Web UI 不改
狀態:Accepted(不變)
Web UI 仍走 /jobs/* 路徑、無 auth。Phase 0.8b 不動。
ADR-010:visionA → converter 改用 pre-shared API key(Phase 0.8b 新增)
狀態:Accepted(2026-05-09 + 2026-05-16 雙重 user 拍板)
背景:
- 5/9 stage e2e 撞 MC 沒註冊
converter:job.read/writescope - converter image 過舊、缺 OAuth middleware
- FAA OAuth 整合狀態不明
- visionA ↔ converter 是 1:1 internal trust,OAuth 過度設計
決定:visionA → converter 改用 pre-shared API key(CONVERTER_API_KEY),constant-time compare。
理由:
- 砍跨團隊依賴(不需要 MC 註冊任何東西)
- visionA 是當前唯一 caller,OAuth 的 scope-based authorization 沒用上
- API key 本身已是 visionA 服務的完整身分證明,scope 概念對 1:1 trust 無意義
代價:
- 砍掉
auth/middleware.js(OAuth resource server)、auth/jwks.js、相關 unit test - 砍
MEMBER_CENTER_ISSUER/JWKS_URL/KNERON_CONVERTER_AUDIENCE/JWKS_*env - 失去 scope-based fine-grained authorization(接受:1:1 trust 不需要)
- 失去多 caller 擴展彈性(未來真有第二個 caller 再加 OAuth 回來)
保留:
- Converter → FAA 仍走 OAuth client_credentials(
files:upload.writescope)→MEMBER_CENTER_TOKEN_URL/KNERON_CONVERTER_CLIENT_*保留 - Promote 流程完全不動
替代方案:詳見 visionA repo adr-015-server-to-server-api-key.md v2.1 §3。
保留設計脈絡的歷史記錄:本 ADR 取代原 ADR-001。git history + visionA repo ADR-015 v2.1 是完整 audit trail。
ADR-011:/result endpoint 採 streaming proxy(Phase 0.8b 新增)
狀態:Accepted(2026-05-16 user 拍板)
背景:見 ADR-005 superseded 改用 /result 中轉。
決定:GET /api/v1/jobs/:id/result 用 Node Stream pipe(res) 把 MinIO GetObjectStream 直接 proxy 回 caller,不 buffer。
理由:
- NEF 可能數百 MB(極端情境),buffer 整個檔會 OOM
- Stream 模式下 Scheduler 記憶體足跡受 Node fetch / HTTP buffer 控制(typical < 64KB per request)
- Client(visionA)可以一邊收一邊處理,TTFB < 500ms 比完整下載時間短
代價:
- Stream error handling 較複雜(headers 已送出後 stream 中斷只能
res.destroy()) - Content-Length 必須在 stream 開始前算好(從 MinIO HEAD 取)
替代方案:
- A. Buffer 整個 NEF 再回(簡單但會 OOM)— 排除
- B. Redirect 到 MinIO presigned URL(簡單但 MinIO 暴露公網風險)— 排除
- C. Stream proxy(選擇)
9. 風險與待確認事項
| # | 風險 / 議題 | 影響 | 行動 |
|---|---|---|---|
| R1 | CONVERTER_API_KEY rotation 流程未自動化 | 低 | Phase 1 接受手動 rotation;外洩時雙端同步改 .env + redeploy |
| R2 | /result 高並發 stream 壓力 |
低 | NEF 通常小、visionA 是唯一 caller、QPS 可控;觀測後再加防護 |
| R3 | visionA 一旦被 compromise 可冒充任意 user_id | 中(接受) | 同 OAuth 模型,本質 trust boundary 不變;audit log + anomaly detection 為主要 mitigation |
| R4 | Sec C1 暫緩(.env 進 git history) | 中 | Phase 1 ready 後做 history rewrite + rotate 所有 secret,包括 CONVERTER_API_KEY |
| R5 | 大檔 multipart OOM(多 user 並發) | 中 | 既有 user_has_active_job 鎖 + MAX_CONCURRENT_UPLOADS semaphore |
| R6 | NEF 7 天過期後 client 重新轉檔 | 低 | API spec 已定義 410 result_expired,visionA 端處理 |
10. Phase 0.8b 切分(架構層)
Phase 0.8b 必做
- API key middleware(
auth/apiKeyMiddleware.js) - 砍 OAuth resource server(
auth/middleware.js/auth/jwks.js) - 保留 OAuth client(
auth/oauthClient.js,promote 用) /api/v1/jobs/:id/resultendpoint- Config 變動(移除 OAuth resource server 相關、新增
CONVERTER_API_KEY) - 4 個既有 endpoint 改掛
requireApiKey()(取代requireAuth(scope)) - README / OpenAPI / .env.example 同步更新
Phase 2 預留(不在本次範圍)
DELETE /api/v1/jobs/:id(仍回 501)POST /api/v1/jobs/:id/download-tokens(仍回 501,未來 MC 補完再啟用)- Webhook
- 觀測強化(Prometheus / OpenTelemetry)
觸發條件
- Phase 0.8b:三方交叉審閱通過 + 使用者審核 + Backend 完成實作 → Reviewer → Testing → 部署
- Phase 2:取決於 visionA 後續需求 + MC owner 時程
11. 後續步驟
- 本 Design Doc 送 PM / Design 交叉審閱
- 使用者審核最終版
- Backend Agent 依
TDD.md索引 +auth.md+api/api-result.md的任務拆分(Phase A 6 個子任務 + Phase B 4 個子任務)增量開發 - Reviewer 每個任務把關 + Testing 整合測試
- 雙端對齊部署(converter 先 → visionA 後 → e2e)
附註:本 Design Doc 約 470 行,未超過拆分門檻。詳細 TDD 內容拆分為 TDD.md 索引 + auth.md + api/*.md + database.md + infra.md + performance.md + observability.md 等子檔案。