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

29 KiB
Raw Blame History

Design Doc — Kneron Model Converter 對外 API

作者Architect Agent

狀態DraftPhase 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.md v2.1 — 為什麼 visionA → converter 改用 pre-shared API key
  • docs/autoflow/04-architecture/adr/adr-016-download-via-converter.md v1.0 — 為什麼 download 改成 converter /result 中轉
  • converter 端的設計沿革見本 repo git log docs/autoflow/04-architecture/design-doc.md

變更歷程

日期 變更 作者
2026-04-25 初版 DraftOAuth 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 clientpromote 用) 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-016caller 端設計脈絡)

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 拍板設計轉向:

  1. 5/9 stage e2e blockerMC 沒註冊 converter:job.read/write scope、converter image 過舊、FAA OAuth 狀態不明 → ADR-015 拍板 visionA ↔ converter 為 1:1 internal trust、改用 pre-shared API key
  2. 5/16 grep MC source:發現 MC 從未實作 /file-access/download-tokens endpoint、delegated download token 鏈從 5/2 寫完到現在一直是斷的 → ADR-016 拍板改成 visionA → converter GET /api/v1/jobs/:id/result 中轉

1.2 生態組成

  • Converter本專案Node.js Task Scheduler + Python Worker部署在靠近 NAS 網段
  • visionA-backendGo 服務Converter 對外 API 的唯一 callerPersona C
  • Member CenterMCOAuth2 / OIDC authorization serverC#)— Phase 0.8b 後 Converter 對外 API 不再經過 MC;但 Converter → FAA 仍以 client_credentials 取 token這條保留
  • File Access AgentFAAtenant 邊界內檔案閘道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/>&lt;API key&gt;| 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 後):

  1. 建 jobvisionA-backend → Convertermultipart/form-data POST /api/v1/jobsAuthorization: Bearer <CONVERTER_API_KEY>。Converter middleware 用 crypto.timingSafeEqual constant-time compare API key通過後把 multipart 寫入 Converter Bucketjobs/{job_id}/input/{filename})。
  2. 轉檔Worker pool 順序處理 onnx → bie → nef Workers 從 Converter Bucket 讀寫,產出結果寫回 Converter Bucket。
  3. PollingvisionA-backend → Converter 每 2-5 秒 GET /api/v1/jobs/:id
  4. Promote 到 NASvisionA-backend → Converter → MC → FAA POST /api/v1/jobs/:id/promote 時 Converter 用既有 OAuth clientcachedfiles:upload.write tokenPUT 結果檔到 FAA。這條 OAuth 鏈條保留不變
  5. DownloadvisionA-backend → ConverterPhase 0.8b 新增 GET /api/v1/jobs/:id/result streaming 從 Converter Bucket 把 NEF binary proxy 回 visionA-backend。不經過 FAA、不經過 MC

2. 目標與非目標 (Goals and Non-Goals)

GoalsPhase 0.8b 後)

  • 對外 APIvisionA → converter用 pre-shared API key 驗證
  • Converter 仍保留 OAuth Client 身分promote 流程用 client_credentials 取 FAA token
  • 提供 5 個對外端點:POST /api/v1/jobsGET /api/v1/jobsGET /api/v1/jobs/:idPOST /api/v1/jobs/:id/promoteGET /api/v1/jobs/:id/result新增
  • /result endpoint 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-GoalsPhase 0.8b 明確不做)

  • 不再做 OAuth resource servervisionA → converter 不驗 MC JWT只有未來真有第二個 caller 才考慮回補)
  • 不做 delegated download tokenMC 沒實作對應 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 SchedulerNode.js Express+ Worker PoolPython 架構
  • 理由
    1. Phase 0.8b 改動範圍是「換 auth middleware + 加一個 streaming endpoint」不足以撐起新服務的運維複雜度
    2. 既有 Crash 即 Reset 哲學對單體有利Scheduler stateless重啟 = 復原
  • 取捨
    • Scheduler 單體承擔 download proxy 的網路 I/ONEF 通常 < 50MBstream 模式下記憶體足跡受 Node fetch / HTTP buffer 控制(不會 buffer 整個檔)
    • 若未來 download QPS 高,可在 Nginx 層加 sendfile / proxy_cache或把 /result 拆出獨立微服務Phase 1 不做)

3.2 Auth 策略:純 API key1: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 trustfull access
code 複雜度 JWKS cache + JWT verify + scope check constant-time string compare
Token rotation MC 端管理 雙方手動同步 .env
適用情境 多 caller、跨組織 單一 caller、同組織

當前 visionA ↔ converter 是 1:1 internal trust同公司、單一 callerOAuth 是 over-engineering。

API key middleware 設計

  • 接受Authorization: Bearer <CONVERTER_API_KEY>(重用既有 Bearer header 格式client / log infra 不變)
  • 比對crypto.timingSafeEqual constant-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 endpointstreaming proxy

為什麼需要

ADR-016 §1MC 沒實作 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)

關鍵設計決定

  1. 不 buffer 整個檔:用 Node stream pipe(res)NEF 可能數百 MB
  2. Content-Length 必須帶visionA 端用來決定 timeout
  3. Filename 規則<source_filename_stem>_<chip>.nef(例:yolov5s.onnx + KL720yolov5s_kl720.neffallback job_<jobID>.nef
  4. 雙路徑 NEF key 解析:支援新格式(result_object_keys.nef+ 舊格式(output.nef_path),對齊 promote 流程的 getJobOutputKey 邏輯
  5. 4xx 情境401invalid API key/ 404job_not_found/ 409job_not_completed/ 410result_expired過 7 天)/ 502storage_unavailable/ 503service_unavailable
  6. Stream error handlingheaders 送出後 stream 失敗 → 只能 res.destroy()、client 看到 ECONNRESETclient req.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.jspromote 用)
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 不動
MinIOConverter Bucket 不動
FAA / MC 不動converter → FAA 仍走 OAuth client_credentials

3.5 技術選型Technology Radar

層級 技術選擇 狀態 選型理由 退出成本
Auth — API key compare crypto.timingSafeEqual Adopt Node 標準庫constant-time
Auth — 取 FAA tokenpromote 自寫 OAuth client + Basic auth + in-memory cache Adopt 既有實作、Phase 1 已驗證
HTTP client 對 FAApromote 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 Setuser:{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_idAPI key 模式下固定 visionA-service)為 key預設 300 req / 5min
  • ETag 支援GET /api/v1/jobs/:id 支援 If-None-Match304 Not Modified 省流量

詳細 API spec 見 TDD.md 索引 + api/*.md 子檔案。

3.7 資料架構

核心資料Redis

Key 類型 內容 TTL
job:{job_id} String (JSON) Job 完整 recorduser_idtenant_idcreated_by_client_idmetadatastage_timingsexpires_atresult_object_keyspromoted_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

不引入 PostgreSQLPhase 1 資料量小(單 user 7 天內 < 10 個 jobRedis 足以承擔。

資料流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 完成 < 5s200MB/ < 12s500MB 詳見 performance.md
POST /api/v1/jobs/:id/promote p95 回應時間 95 百分位 < 3s PRD §9.2.1
GET /api/v1/jobs/:id/result p95 TTFBtime 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 即 ResetWorker 重啟後繼續 consume Redis Stream
Redis Crash 符合「Crash 即 Reset」設計哲學所有 job 遺失
Scheduler Crash 重啟後繼續服務

4.3 災難復原

維持既有 Crash 即 ResetRPO = 無保證、RTO < 30sDocker restart


5. 安全架構 (Security)

詳見 security.md。本節僅列高層原則。

5.1 威脅模型STRIDE 摘要Phase 0.8b 版)

威脅 風險 防護
Spoofing偽造 visionA-backend API key constant-time compareTLS protected 傳輸
Spoofing偽造 user_id 中(接受) 信任 visionA-backend 的 user_idConverter 不做 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 3264 hex chars
  • 部署:放 .env / docker-compose env / k8s secret
  • 雙端對齊visionA VISIONA_CONVERTER_API_KEY = converter CONVERTER_API_KEY
  • Rotation 策略每環境獨立dev / stage / prod外洩時雙端同步 rotate + redeploy
  • 絕不進 git / Slack / email / loglog 只記 api_key_lengthapi_key_set: true boolean

5.3 部署分流

維持既有設計Nginx 雙 vhostpublic /api/v1/* + internal /jobs/*)。詳見 infra.md


6. 效能工程 (Performance)

詳見 performance.md。重點:

  • POST /api/v1/jobs p95 < 5s200MB / < 12s500MB
  • GET /api/v1/jobs/:id p95 < 200ms
  • GET /api/v1/jobs/:id/result TTFB < 500msPhase 0.8b 新增 SLO
  • POST /api/v1/jobs/:id/promote p95 < 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-010Phase 0.8b

原因5/9 stage e2e 撞 MC 沒註冊 scopevisionA ↔ converter 為 1:1 internal trust、OAuth 為 over-engineering。

→ visionA repo adr-015-server-to-server-api-key.md v2.1 記錄完整轉向理由。


ADR-002promote 結果檔採「做法 2」— Converter 自己推到 FAA

狀態Accepted不變Phase 0.8b 保留)

範圍:本 ADR 只針對結果檔 promote 的搬檔路徑;原始模型不會進 FAA。Converter → FAA 仍走 OAuth client_credentials + files:upload.write scope。

詳細決策內容沿用前版5/2 寫入)。


ADR-003user_id 以 multipart 欄位傳遞

狀態Accepted不變Phase 0.8b 保留)

API key 模式下user_id 仍是 visionA-backend 傳的 multipart field。Trust boundary 假設不變visionA-backend 內部受控)。


ADR-004Polling 而非 Webhook

狀態Accepted不變


ADR-005Phase 1 使用者下載改用 /result 中轉(取代原 delegated download token 設計

狀態AcceptedPhase 0.8b 拍板,取代原 ADR-005「Phase 1 使用者下載延至 Phase 2」

背景5/16 grep MC source 發現 MC 從未實作 /file-access/download-tokens endpointFAA 的 MemberCenterDelegatedDownloadTokenValidator.cs 假設 MC 有對應 introspection endpoint也是假設錯了。delegated download token 鏈從 5/2 寫完到現在一直是斷的。

決定:不動 MC、不動 FAA。改設計成 visionA → converter GET /api/v1/jobs/:id/result 中轉。

理由

  1. MC owner 時程不可控,延 Phase 2 不可行
  2. NEF 通常 < 50MBstreaming proxy 對 Converter Scheduler 不重
  3. 走 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 §36 個方案完整分析)。


ADR-006Phase 1 Web UI 不改

狀態Accepted不變

Web UI 仍走 /jobs/* 路徑、無 auth。Phase 0.8b 不動。


ADR-010visionA → converter 改用 pre-shared API keyPhase 0.8b 新增

狀態Accepted2026-05-09 + 2026-05-16 雙重 user 拍板)

背景

  1. 5/9 stage e2e 撞 MC 沒註冊 converter:job.read/write scope
  2. converter image 過舊、缺 OAuth middleware
  3. FAA OAuth 整合狀態不明
  4. visionA ↔ converter 是 1:1 internal trustOAuth 過度設計

決定visionA → converter 改用 pre-shared API keyCONVERTER_API_KEYconstant-time compare。

理由

  1. 砍跨團隊依賴(不需要 MC 註冊任何東西)
  2. visionA 是當前唯一 callerOAuth 的 scope-based authorization 沒用上
  3. 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_credentialsfiles:upload.write scopeMEMBER_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 proxyPhase 0.8b 新增

狀態Accepted2026-05-16 user 拍板)

背景:見 ADR-005 superseded 改用 /result 中轉。

決定GET /api/v1/jobs/:id/result 用 Node Stream pipe(res) 把 MinIO GetObjectStream 直接 proxy 回 caller不 buffer。

理由

  1. NEF 可能數百 MB極端情境buffer 整個檔會 OOM
  2. Stream 模式下 Scheduler 記憶體足跡受 Node fetch / HTTP buffer 控制typical < 64KB per request
  3. ClientvisionA可以一邊收一邊處理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_expiredvisionA 端處理

10. Phase 0.8b 切分(架構層)

Phase 0.8b 必做

  • API key middlewareauth/apiKeyMiddleware.js
  • 砍 OAuth resource serverauth/middleware.js / auth/jwks.js
  • 保留 OAuth clientauth/oauthClient.jspromote 用)
  • /api/v1/jobs/:id/result endpoint
  • 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. 後續步驟

  1. 本 Design Doc 送 PM / Design 交叉審閱
  2. 使用者審核最終版
  3. Backend Agent 依 TDD.md 索引 + auth.md + api/api-result.md 的任務拆分Phase A 6 個子任務 + Phase B 4 個子任務)增量開發
  4. Reviewer 每個任務把關 + Testing 整合測試
  5. 雙端對齊部署converter 先 → visionA 後 → e2e

附註:本 Design Doc 約 470 行,未超過拆分門檻。詳細 TDD 內容拆分為 TDD.md 索引 + auth.md + api/*.md + database.md + infra.md + performance.md + observability.md 等子檔案。