docs(autoflow): ADR-016 — visionA download 改走 converter GetResult,撤回 FAA delegated token 鏈
致命發現(grep MC + FAA source 確認):
- MC source 沒有 issue delegated download token endpoint
- MC source 沒有 validate delegated download token endpoint
- FAA MemberCenterDelegatedDownloadTokenValidator.cs 假設的 MC introspection endpoint 不存在
- ADR-014 §2 從 5/2 寫完到現在這條鏈一直是斷的、只是因為從未實際 e2e 跑通過所以沒被發現
使用者拍板硬約束:不動 MC + 不動 FAA
新增 ADR-016:
- visionA download 改用 converter GET /api/v1/jobs/{id}/result(新 endpoint)
- visionA backend 用既有 ConverterAPIKey 認證(不需新增 secret)
- 維持 T4 已實作的 stream proxy 結構(io.CopyN + Content-Disposition + size cap)
- promote 仍 PUT FAA(converter 內部用自己的 OAuth、與 visionA 無關)
- 不需動 MC + FAA + warrenchen
- 6 個替代方案逐一說明排除理由
修訂既有文件:
- ADR-014 v1.1 → v1.2:§2 download flow 標註被 ADR-016 部分 supersede
- ADR-015 v2.0 → v2.1:§2 visionA → FAA delegated token 設計(v2.0 從 v1.x 撤回的設計)再次撤回;§9 env 表撤回 v2.0 加回的 OIDC ServiceClient* / TenantID / FAABaseURL;visionA 端 server-to-server 只剩 ConverterAPIKey 一把
- conversion.md v0.5 → v0.6:§1 sequence diagram 重畫(移除 MC node)、§2 模組設計(mc_token_client.go 整檔刪除確認、faa_client.go 改名 converter_result_client.go)、§3.2 visionA → FAA 整段標撤回、§4.1 download handler 改 converter.GetResult、§6 錯誤碼撤回 mc/faa 三個 code 加 result_not_found / result_expired
- api-conversion.md v0.5 → v0.6:檔頭 Auth 段落改寫、§4 download endpoint 改述、error code 表撤回 mc_token_unavailable / download_token_failed
- oidc-tdd.md v0.3 → v0.4:§13.1 環境變數表 OIDC ServiceClient* / TenantID / FAABaseURL 從「重新啟用」改回「再次廢棄」、§13.1.1 stage env 範例移除 service client / tenant_id / FAA URL、§13.1.3 改寫為「v0.4 單線設計」說明
整體影響:
- 不需復活 mc_token_client.go(commit 86b7175 砍除狀態維持)
- 不需復活 OIDCConfig.ServiceClientID/Secret/TenantID(commit 86b7175 移除狀態維持)
- visionA backend faa_client.go 要改名為 converter_result_client.go、改呼叫 converter.GetResult
- visionA backend flow.go DownloadStream / PromoteToModels 改用 converter.GetResult
- jimchen 跨 repo 任務:converter scheduler 加 GET /api/v1/jobs/{id}/result endpoint
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
86b7175649
commit
dab13ed984
@ -1,7 +1,13 @@
|
||||
# ADR-014:visionA 端轉檔功能架構(Phase 0.8)
|
||||
|
||||
## 狀態
|
||||
Accepted — 2026-04-30
|
||||
Accepted — 2026-04-30 / **§2 download flow 部分 supersede — 2026-05-16(ADR-016)**
|
||||
|
||||
> **2026-05-16 更新**:§2「Download — FAA delegated token(browser 直連 / v1.1 後 server-side proxy)」整段被 [ADR-016](./adr-016-download-via-converter.md) 部分 supersede。原因:對 MC source 完整驗證後發現「MC issue + validate delegated download token」endpoint **從未存在**——本 §2 從 2026-05-02 寫定起即為 broken design,只是因從未 e2e 跑通 visionA → FAA download 而未被發現。
|
||||
>
|
||||
> **新設計**:visionA download 改走 converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉(jimchen 可單方控制兩端、不必動 MC / FAA / warrenchen)。詳見 ADR-016。
|
||||
>
|
||||
> **本 ADR 仍有效的段落**:§1(upload streaming proxy)、§3(半自動分流的「加到模型庫」原則,但 server-side pull 的 FAA 部分改走 converter)、§4(模組劃分)、§5(service token cache 僅 converter 部分已被 ADR-015 §1 supersede;FAA 部分被 ADR-016 supersede)、§6(user_id trust boundary,**核心原則完全不變**)、§7(FAA / MC 相關 row 被 ADR-016 supersede;converter row 維持)、§8(active job 衝突處理不變)。
|
||||
|
||||
## 上位 / 同層 ADR
|
||||
- 沿用:[ADR-006](./adr-006-no-redis-in-prototype.md)(in-memory state)、[ADR-010](./adr-010-oidc-bff.md)(OIDC BFF + confidential client)、[ADR-011](./adr-011-supersede-adr-005.md)(OIDC 取代 StaticAuth)、[ADR-013](./adr-013-public-client.md)(user OIDC client 為 public + PKCE-only;service client 仍為 confidential)
|
||||
@ -53,6 +59,20 @@ Browser ──multipart──► visionA backend ──multipart streaming──
|
||||
|
||||
### 2. Download — 多次性 → FAA delegated token(server-side 302 redirect → browser 直連 FAA)
|
||||
|
||||
> ⚠️ **2026-05-16:本節整段被 [ADR-016](./adr-016-download-via-converter.md) supersede**。
|
||||
>
|
||||
> 致命發現(2026-05-16):
|
||||
> 1. MC source 沒有 `POST /file-access/download-tokens` endpoint(無法 issue delegated token)
|
||||
> 2. MC source 沒有 `IDelegatedDownloadTokenValidator` 對應的 introspection endpoint(即使有 token 也無法 validate)
|
||||
> 3. FAA `GET /files/{key}` 強制只接 delegated token、不接 service token(visionA 即使能拿 service token 也打不進去)
|
||||
>
|
||||
> 本節下方描述的「server-side 302 redirect」/「跟 MC 換 delegated token」/「FAA 跟 MC validate」整條鏈從 2026-05-02 起即為 broken design、從未 e2e 跑通。
|
||||
>
|
||||
> **新設計(ADR-016)**:visionA download 改走 converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉。不再有 visionA → MC、visionA → FAA 任何 server-to-server 路徑。
|
||||
>
|
||||
> 本節以下內容**僅作歷史保留**、實作以 ADR-016 為準。
|
||||
|
||||
|
||||
```
|
||||
Browser ──GET /api/conversion/{job_id}/download──► visionA backend
|
||||
↓
|
||||
@ -230,3 +250,4 @@ converter 回 `409 user_has_active_job` → visionA-backend 透傳 `409 active_j
|
||||
|------|------|------|
|
||||
| 2026-04-30 | 1.0 | 初版 — Phase 0.8 轉檔整合架構決策 |
|
||||
| 2026-04-30 | 1.1 | Download flow 改為 server-side HTTP 302 redirect(仿 FAA TestSite `DownloadFileDirect`),token 不過 frontend JS、不需 FAA CORS |
|
||||
| 2026-05-16 | 1.2 | §2 download flow 整段標 supersede by [ADR-016](./adr-016-download-via-converter.md):致命發現 MC source 沒有 issue / validate delegated download token endpoint、§2 從 2026-05-02 起即為 broken design。新設計 visionA download 改走 converter `GET /api/v1/jobs/{id}/result` + visionA stream 中轉。其他段落(§1 upload streaming proxy / §3 半自動分流 / §4 模組劃分 / §6 user_id trust boundary / §8 active job 衝突處理)維持有效;§5 / §7 中 FAA / MC 相關部分連帶 supersede。 |
|
||||
|
||||
@ -1,12 +1,22 @@
|
||||
# ADR-015:visionA → converter / FAA 採 pre-shared API key 認證(取代 OAuth client_credentials)
|
||||
# ADR-015:visionA → converter 採 pre-shared API key 認證(取代 OAuth client_credentials)— 範圍縮限至 visionA → converter
|
||||
|
||||
## 狀態
|
||||
Accepted — 2026-05-11
|
||||
Accepted — 2026-05-11 / **範圍縮限 — 2026-05-16 (v2.0)** / **§2 visionA → FAA 整段再次撤回 — 2026-05-16 (v2.1)**
|
||||
|
||||
> **v2.1 撤回摘要(2026-05-16 下午)**:v2.0 §2「visionA → FAA 回到 ADR-014 §2(MC service token + delegated download token)」**整段再次撤回**。原因:對 MC source 完整驗證後發現 MC **從未有** issue / validate delegated download token endpoint—— ADR-014 §2 從 2026-05-02 起即為 broken design、v2.0 沿用該設計也是 fictional。
|
||||
>
|
||||
> **v2.1 新設計**:visionA download 改走 [ADR-016](./adr-016-download-via-converter.md)(converter 新增 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉)。visionA 端不再有任何 visionA → MC / visionA → FAA 的 server-to-server 路徑。**visionA → converter API key 路線(§1)維持完全不變**。
|
||||
>
|
||||
> **v2.0 範圍縮限摘要(保留歷史)**:v1.x 原本決策「visionA → converter / FAA 兩條 server-to-server 線都改 API key」。2026-05-16 使用者拍板**撤回 visionA → FAA 改 API key 部分**,FAA 線回到 ADR-014 §2 原設計。**v2.0 後 v2.1 再次撤回** — FAA 線完全交由 ADR-016 取代(converter 中轉,不再有 visionA → FAA / visionA → MC 鏈)。
|
||||
|
||||
## 上位 / 同層 ADR
|
||||
|
||||
- **部分 supersedes**:[ADR-014](./adr-014-conversion-integration.md) §5「Service token cache — 仿 converter scheduler 模式」、§6「user_id 注入 + trust boundary」(OAuth service token 段落)、§7「失敗模式 retry 矩陣」(MC token / MC delegated token 兩個 row)。ADR-014 的其他段落(upload streaming proxy、download 走 302 redirect、半自動 promote、不擴 model schema、模組劃分)仍有效。
|
||||
- **不影響**:[ADR-013](./adr-013-public-client.md) — user login 的 OIDC public PKCE client 仍照舊。本 ADR 只動「server-to-server」這條線;「user login」這條線完全不變。
|
||||
- **部分 supersedes**:[ADR-014](./adr-014-conversion-integration.md) §5「Service token cache — 仿 converter scheduler 模式」中 **converter 部分**(visionA → converter 的 service token / scope `converter:job.write/read` 取消)、§7「失敗模式 retry 矩陣」中 converter MC token row(converter 線不再經 MC)。
|
||||
- **v2.0 修正(保留歷史)**:v2.0 一度把 FAA 部分維持 ADR-014 §2 原設計,但 **v2.1 整段撤回**——FAA 部分的 supersede 改由 [ADR-016](./adr-016-download-via-converter.md) 接手(visionA download 改走 converter result endpoint,視 ADR-014 §2 / 本 ADR v2.0 §2 為 broken design)。
|
||||
- **v2.1 後**:本 ADR 對 ADR-014 的 supersede 範圍維持「§5 中 converter 部分 / §7 中 converter MC token row」;ADR-014 §2 / §5 中 FAA 部分 / §6 / §7 中 FAA + MC delegated token row 由 ADR-016 統一 supersede。
|
||||
- ADR-014 的其他段落(upload streaming proxy、半自動 promote 原則、不擴 model schema、模組劃分)仍有效。
|
||||
- **被 supersede**(v2.1 新增):本 ADR §2「visionA → FAA」整段被 [ADR-016](./adr-016-download-via-converter.md) supersede。visionA download 不再有 visionA → FAA / visionA → MC 任何鏈路。
|
||||
- **不影響**:[ADR-013](./adr-013-public-client.md) — user login 的 OIDC public PKCE client 仍照舊。本 ADR v2.1 後只動「server-to-server visionA → converter」這條線;「user login」維持不變;「server-to-server visionA → FAA」這條線**直接不存在**(由 ADR-016 撤回)。
|
||||
- 沿用:[ADR-006](./adr-006-no-redis-in-prototype.md)(in-memory state)、[ADR-010](./adr-010-oidc-bff.md)(user login 的 OIDC BFF Pattern)、[ADR-011](./adr-011-supersede-adr-005.md)(取代 StaticAuth)
|
||||
|
||||
## 背景 (Context)
|
||||
@ -31,35 +41,51 @@ Phase 0.8 stage 部署實際跑到才發現**整條鏈路有 4 個串行 blocker
|
||||
|
||||
要修齊這 4 個 blocker 必須跨 3 個 repo 改 + 同步 MC team + redeploy converter + warrenchen 配合 FAA — 對「2 條 1:1 trust 關係的 server-to-server 整合」明顯過度設計。
|
||||
|
||||
### 過度設計的訊號
|
||||
### v1.x 過度設計的訊號(converter 線適用)
|
||||
|
||||
OAuth `client_credentials` + JWKS + scope 機制適合的場景:
|
||||
|
||||
- **多個 client 對同一個 resource server**(如 SaaS 平台對外開放 API、第三方 dev 串接),需要區分不同 client 權限、需要短 TTL token 限制 blast radius、需要 scope 細粒度
|
||||
- **不同團隊 / 不同信任邊界**,client 端的 secret 不能由 server 端管理
|
||||
|
||||
visionA → converter / FAA 的場景**完全不符合上面任一個**:
|
||||
**visionA → converter** 的場景**完全不符合上面任一個**:
|
||||
|
||||
- **1:1 trust 關係**(visionA 是 converter / FAA 唯一的 server-to-server caller,沒有第三方)
|
||||
- **使用者同時維護 visionA + converter**(jimchen),FAA 由 warrenchen 維護(同公司、可協調)
|
||||
- **1:1 trust 關係**(visionA 是 converter 唯一的 server-to-server caller,沒有第三方)
|
||||
- **使用者同時維護 visionA + converter**(jimchen),可單方拍板改 middleware
|
||||
- **全部 internal trust**(不是給外部 dev 用,沒有 untrusted client)
|
||||
- **無需 scope 細分**(converter 只關心「是否為 visionA」、FAA 只關心「是否為 visionA」,單一布林)
|
||||
- **無需 scope 細分**(converter 只關心「是否為 visionA」,單一布林)
|
||||
|
||||
而成本:
|
||||
|
||||
- **複雜度**:MC 端要 onboard scope、issuer JWKS 要可達、token cache 要寫對、TTL 邊界要處理、4 個 5xx / 4xx 失敗模式要 retry & graceful degrade
|
||||
- **鏈路長度**:visionA → MC → cache → converter / FAA,任一節點掛掉都不能轉檔
|
||||
- **鏈路長度**:visionA → MC → cache → converter,任一節點掛掉都不能轉檔
|
||||
- **可觀測性負擔**:要追的 metrics 包含 token cache hit rate、MC 失敗率、scope 對齊狀態
|
||||
|
||||
### 已洩漏的 stage service client secret(觀察事實)
|
||||
### 為什麼 v2.0 撤回 FAA 改 API key(FAA 線適用相反邏輯)
|
||||
|
||||
`RciRUyiCkbd60ikkZGkfQ2xV4r02VW3/j0ASKV/DD/E=` 已在對話中外洩(progress.md 2026-05-11 紀錄)。改用 API key 後此 secret 直接作廢、不需 rotate,這是 Phase 0.8b 的順帶收益。
|
||||
v1.0 把同一套「過度設計」邏輯外推到 FAA,但 FAA 線的實際情況不同:
|
||||
|
||||
1. **FAA 是 warrenchen 維護的公司共用 repo**:協調成本不對稱(converter 是 jimchen 自己的、可單方改;FAA 改一行都要走跨人協調)
|
||||
2. **MC 端針對 FAA 的 scope 早已備妥**:使用者於 2026-05-16 提供的 stage service client(`4242ba63099d4f318dd3f143d27ef4c5`)含 `files:upload.write files:metadata.read files:delete files:download.delegate` 4 個 scope,**完整 cover FAA 4 個 endpoint**(PUT / GET metadata / HEAD / DELETE / GET file),不需要 MC team 額外 onboard
|
||||
3. **FAA 已內建 dual-auth 設計**:`/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` line 184-254 顯示 `GET /files/{key}` 下載 endpoint **沒掛 `RequireAuthorization()`**,改用 `IDelegatedDownloadTokenValidator.ValidateAsync(...)` 驗 delegated download token;其他 endpoint(`PUT` / `metadata` / `HEAD` / `DELETE`)才走 JWT Bearer + `EnsureJwtScopeAndTenant`。
|
||||
→ **FAA 設計上 download 就是要 delegated token、不接 service token**;如果硬要把 FAA 全 endpoint 改成 API key middleware,等於要 warrenchen 重寫整套 dual-auth、把既有 delegated token validator 拔掉,遠超「補 middleware」的成本
|
||||
4. **5/9 撞 MC scope 沒註冊的痛主因在 converter 線**(`converter:job.read/write` 兩個 scope 不存在);FAA 線 4 個 scope 已備妥的事實在當時尚未驗證
|
||||
5. **converter 線 v1.0 改 API key 已能解決「最痛的」blocker**(不必動 MC、不必動 FAA、不必跨人);FAA 線本就 1:N(FAA 之後可能服務多個 visionA-like 產品線),維持 OAuth 框架對 FAA 端架構演進更友善
|
||||
|
||||
→ v2.0 縮限至「**只動 converter 線;FAA 線回到 ADR-014 §2 原設計**」是更精確的責任邊界劃分。
|
||||
|
||||
### 已洩漏的 stage service client secret(v1.x 觀察事實 — v2.0 部分仍適用)
|
||||
|
||||
`RciRUyiCkbd60ikkZGkfQ2xV4r02VW3/j0ASKV/DD/E=` 已在對話中外洩(progress.md 2026-05-11 紀錄)。
|
||||
|
||||
- **v1.x 的處理**:改用 API key 後此 secret 直接作廢、不需 rotate
|
||||
- **v2.0 的處理**:FAA 線改回 service token 路線後,使用者於 2026-05-16 提供的**新** stage service client `4242ba63099d4f318dd3f143d27ef4c5` 取代舊 client(舊的洩漏值仍作廢、不重用);新 secret **僅放 stage host `.env.stage` 與部署 secret store、絕不進 git / 文件**(本 ADR、TDD、env example 一律不寫真實 secret 值)
|
||||
|
||||
## 決策 (Decision)
|
||||
|
||||
採 **pre-shared API key + `Authorization: Bearer <api-key>` header** 取代 OAuth client_credentials 服務間認證。
|
||||
### v2.0 範圍:採 **pre-shared API key + `Authorization: Bearer <api-key>` header** 取代 OAuth client_credentials,**僅適用於 visionA → converter**。visionA → FAA 維持 ADR-014 §2 原設計(MC service token + delegated download token)。
|
||||
|
||||
### 1. visionA → converter
|
||||
### 1. visionA → converter(v1.0 採用 / v2.0 維持)
|
||||
|
||||
```
|
||||
visionA backend 啟動時
|
||||
@ -86,49 +112,103 @@ subtle.ConstantTimeCompare(token, CONVERTER_API_KEY)
|
||||
match → 放行;mismatch → 401
|
||||
```
|
||||
|
||||
### 2. visionA → FAA
|
||||
### 2. ⚠️ visionA → FAA(v2.1:整段撤回,改走 ADR-016 converter 中轉)
|
||||
|
||||
對稱設計:
|
||||
> **v2.1 撤回(2026-05-16 下午)**:v2.0 在本節「FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token)」**整段撤回**。
|
||||
>
|
||||
> **理由(致命發現 2026-05-16)**:
|
||||
> 1. MC source 沒有 `POST /file-access/download-tokens` endpoint(visionA 無法跟 MC 換 delegated token)
|
||||
> 2. MC source 沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint(即使有 token 也無法 validate)
|
||||
> 3. FAA `GET /files/{key}` 強制只接 delegated token、不接 service token
|
||||
>
|
||||
> → ADR-014 §2 與本 ADR v2.0 §2 描述的「visionA → MC → FAA delegated token 鏈」**完全是 fictional**(從 2026-05-02 起未曾 e2e 跑通過)。
|
||||
>
|
||||
> **v2.1 採用的設計**:visionA download 改走 [ADR-016](./adr-016-download-via-converter.md)(converter 新增 `GET /api/v1/jobs/{id}/result` endpoint + visionA stream 中轉)。visionA 端不再有任何 visionA → MC server-to-server 路徑、不再有任何 visionA → FAA 直接呼叫。
|
||||
>
|
||||
> 本節以下內容**僅作歷史保留**、實作以 ADR-016 為準;v2.1 後對應的 visionA 端 code 應撤回(mc_token_client.go 不需復活,已於 commit `86b7175` 砍除 → 維持砍除;OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID 維持 v1.x 廢棄狀態)。
|
||||
|
||||
**v2.0 原採用的設計(即 ADR-014 §2 原設計,v2.1 整段撤回,僅作歷史保留)**:
|
||||
|
||||
```
|
||||
visionA backend 啟動時
|
||||
↓
|
||||
讀 env VISIONA_FAA_API_KEY
|
||||
讀 OIDCConfig.ServiceClientID / ServiceClientSecret + ConversionConfig.TenantID
|
||||
↓
|
||||
[加到模型庫流程要 pull NEF]
|
||||
[需要打 FAA,例如「加到模型庫」server-to-server pull、或「下載」proxy]
|
||||
↓
|
||||
打 FAA:
|
||||
Authorization: Bearer <VISIONA_FAA_API_KEY>
|
||||
visionA → MC POST {issuer}/oauth/token
|
||||
grant_type=client_credentials
|
||||
client_id=<ServiceClientID>
|
||||
client_secret=<ServiceClientSecret>
|
||||
scope=files:upload.write files:metadata.read files:delete files:download.delegate
|
||||
↓
|
||||
MC 回 service access_token(cache 至 exp - 15s)
|
||||
↓
|
||||
A 路線(FAA 寫 / metadata / delete / s2s download):
|
||||
visionA 帶 Authorization: Bearer <service-token> 打 FAA
|
||||
FAA 端 .RequireAuthorization() + EnsureJwtScopeAndTenant 驗
|
||||
├─ JWT 簽章(FAA `AddJwtBearer` Authority = MC issuer,自動 JWKS)
|
||||
├─ Audience(FAA `Auth:Audience`)
|
||||
├─ scope claim(PUT 要 files:upload.write;GET metadata / HEAD 要 files:metadata.read;DELETE 要 files:delete)
|
||||
└─ tenant_id claim 必須等於 instanceOptions.TenantId
|
||||
→ 通過則放行
|
||||
|
||||
B 路線(download stream proxy — visionA backend 中轉到 browser):
|
||||
1. visionA 帶 service-token 打 MC POST /file-access/download-tokens
|
||||
(scope: files:download.delegate;針對特定 object_key + GET method + 5 分鐘 TTL)
|
||||
2. MC 回 delegated download token (opaque)
|
||||
3. visionA 帶 Authorization: Bearer <delegated-token> 打 FAA GET /files/{key}
|
||||
4. FAA 端 GET /files/{key} 沒掛 .RequireAuthorization(),
|
||||
改走 IDelegatedDownloadTokenValidator.ValidateAsync(...)
|
||||
├─ token active
|
||||
├─ tenant_id match
|
||||
├─ object_key match
|
||||
└─ method == "GET"
|
||||
→ 通過則 stream NEF binary
|
||||
5. visionA backend io.CopyN(...) 中轉回 browser
|
||||
```
|
||||
|
||||
FAA 端 middleware:
|
||||
**為什麼 v2.0 設計「download 線必須是 delegated token」而非「service token」**:
|
||||
|
||||
```
|
||||
讀 env FAA_API_KEY
|
||||
↓
|
||||
[收到請求]
|
||||
↓
|
||||
parse Authorization header → 取 token
|
||||
↓
|
||||
constant-time compare(C# `CryptographicOperations.FixedTimeEquals` 或等效)
|
||||
↓
|
||||
match → 放行;mismatch → 401
|
||||
```
|
||||
FAA 端的 dual-auth 設計(已實作於 `/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs`)強制如此:
|
||||
|
||||
### 3. 每個下游各自獨立的 key
|
||||
| FAA endpoint | line range | auth 機制 | 適用 scope / token type |
|
||||
|--------------|-----------|----------|------------------------|
|
||||
| `GET /files/metadata/{**objectKey}` | 80-111 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:metadata.read` (service token) |
|
||||
| `HEAD /files/{**objectKey}` | 113-148 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:metadata.read` (service token) |
|
||||
| `PUT /files/{**objectKey}` | 150-182 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:upload.write` (service token) |
|
||||
| **`GET /files/{**objectKey}`** | **184-254** | **無 `.RequireAuthorization()`;用 `IDelegatedDownloadTokenValidator.ValidateAsync(...)`** | **`files:download.delegate` 換出來的 delegated token** |
|
||||
| `DELETE /files/{**objectKey}` | 256-287 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:delete` (service token) |
|
||||
|
||||
**不共用一把**:
|
||||
→ FAA `GET /files/{key}` **不接 service token**,必須用 MC 簽的 delegated download token。
|
||||
→ visionA download flow 必須做「換 delegated token」這一步、不能省。
|
||||
→ visionA「加到模型庫」server-to-server pull 流程因為走的也是 `GET /files/{key}` 下載端點,**也要走 delegated download token 路徑**(v2.0 修正:v1.x 與 ADR-014 §3 「scope `files:download.read`」描述不精確;FAA 端 source 真相是 download endpoint 一律用 delegated token,scope `files:download.delegate` 是 service client 用來「向 MC 換 delegated token」的能力,不是 FAA 端 endpoint 接收的 scope)
|
||||
|
||||
**stage 端證據(使用者 2026-05-16 提供)**:
|
||||
|
||||
- FAA stage URL:`https://stage-9527.innovedus.com:5081`
|
||||
- TenantId:`732270c0-449c-489c-bfad-321e9bf89b3d`
|
||||
- ServiceClientId:`4242ba63099d4f318dd3f143d27ef4c5`(取代 v1.x 提到的舊 client `23605e14...`)
|
||||
- ServiceScopes:`files:upload.write files:metadata.read files:delete files:download.delegate`
|
||||
- ServiceClientSecret:放 stage host `.env.stage`,不進 git / 文件
|
||||
|
||||
**待 verify(合規性段落追蹤)**:MC stage 端是否確實註冊上述 4 個 scope 並對該 service client 生效,需 stage redeploy 前實測 `POST /oauth/token` 拿到含 4 scope 的 access_token。
|
||||
|
||||
### 3. 單一下游:converter(v1.x「每個下游各自獨立的 key」表格 v2.0 縮限)
|
||||
|
||||
**v1.x 的 key 表格** 縮限至 converter 一條:
|
||||
|
||||
| Key | 持有者 | 用途 |
|
||||
|-----|--------|------|
|
||||
| `VISIONA_CONVERTER_API_KEY`(visionA 端) / `CONVERTER_API_KEY`(converter 端) | jimchen | visionA → converter |
|
||||
| `VISIONA_FAA_API_KEY`(visionA 端) / `FAA_API_KEY`(FAA 端) | jimchen / warrenchen | visionA → FAA |
|
||||
|
||||
理由:每條 trust boundary 各自獨立,一條 rotate 不影響另一條;converter / FAA 不應該因為對方洩漏被連坐。
|
||||
**v1.x 中的 FAA key row(`VISIONA_FAA_API_KEY` / `FAA_API_KEY`)撤回**——FAA 改回 MC service token 路徑、不需要 pre-shared API key。
|
||||
|
||||
### 3.5 Reference Middleware Implementation
|
||||
理由(converter 維持):每條 trust boundary 各自獨立。converter 線 1:1 trust,雙方都由 jimchen 維護,rotate 對齊成本可控。
|
||||
|
||||
本節提供兩端 middleware 的可直接照抄 reference snippet。converter 端 jimchen 跨 repo 用 Go 實作;FAA 端 warrenchen 跨 repo 用 C# 實作。兩端的設計原則一致:constant-time compare、統一 401、不洩漏 token、不洩漏「key 對 / 不對」的差異。
|
||||
### 3.5 Reference Middleware Implementation(v2.0 縮限至 converter 端)
|
||||
|
||||
本節提供 converter 端 middleware 的可直接照抄 reference snippet。
|
||||
|
||||
#### 3.5.1 converter 端(Go — net/http 標準 middleware pattern)
|
||||
|
||||
@ -232,208 +312,24 @@ authMiddleware := middleware.NewAPIKeyAuth(expectedKey, logger)
|
||||
mux.Handle("/api/v1/", authMiddleware(apiHandler))
|
||||
```
|
||||
|
||||
#### 3.5.2 FAA 端(C# ASP.NET Core middleware)
|
||||
#### 3.5.2 ~~FAA 端(C# ASP.NET Core middleware)~~(v2.0 撤回 — 整段刪除)
|
||||
|
||||
`CryptographicOperations.FixedTimeEquals` 是 .NET 6+ 提供的 constant-time byte 比較函式。下方提供 classic Middleware class 寫法(推薦給既有 ASP.NET Core MVC / Web API 專案)+ minimal API 的 inline middleware 寫法(推薦給 .NET 8 minimal API 新 project)。warrenchen 視 FAA 既有架構選一即可。
|
||||
> **v2.0 撤回**:v1.1 在此處提供的 FAA 端 ASP.NET Core middleware snippet(含 Classic Middleware Class 寫法 A + Minimal API Inline Middleware 寫法 B)**整段刪除**。
|
||||
>
|
||||
> **理由**:FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token)後,FAA 端**不需要新增任何 API key middleware**:
|
||||
>
|
||||
> - 既有 `AddJwtBearer` + `RequireAuthorization()` + `EnsureJwtScopeAndTenant` 已涵蓋 PUT / metadata / HEAD / DELETE 4 個 endpoint
|
||||
> - 既有 `IDelegatedDownloadTokenValidator` 已涵蓋 GET download endpoint
|
||||
> - **FAA 端零變更**——這是 v2.0 撤回的核心收益(不必動公司共用 FAA repo、不必跟 warrenchen 協調)
|
||||
|
||||
**寫法 A:Classic Middleware Class(推薦既有 ASP.NET Core 專案)**
|
||||
#### 3.5.3 部署檢查清單(v2.0 縮限至 converter 端)
|
||||
|
||||
```csharp
|
||||
// Middleware/ApiKeyAuthMiddleware.cs
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FAA.Middleware;
|
||||
|
||||
public class ApiKeyAuthMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ApiKeyAuthMiddleware> _logger;
|
||||
private readonly byte[] _expectedKeyBytes;
|
||||
|
||||
private const string BearerPrefix = "Bearer ";
|
||||
|
||||
public ApiKeyAuthMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<ApiKeyAuthMiddleware> logger,
|
||||
string expectedKey)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
// 啟動時 fail-fast — 不允許 server 在沒有 key 的狀態下啟動
|
||||
if (string.IsNullOrEmpty(expectedKey))
|
||||
{
|
||||
throw new InvalidOperationException("FAA_API_KEY env not set");
|
||||
}
|
||||
_expectedKeyBytes = Encoding.UTF8.GetBytes(expectedKey);
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var authHeader = context.Request.Headers.Authorization.ToString();
|
||||
|
||||
// missing Authorization header
|
||||
if (string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"api key auth failed: reason=missing_authorization_header path={Path} remote={Remote}",
|
||||
context.Request.Path, context.Connection.RemoteIpAddress);
|
||||
await WriteUnauthorizedAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 不是 Bearer prefix
|
||||
if (!authHeader.StartsWith(BearerPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"api key auth failed: reason=missing_bearer_prefix path={Path} remote={Remote}",
|
||||
context.Request.Path, context.Connection.RemoteIpAddress);
|
||||
await WriteUnauthorizedAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = authHeader[BearerPrefix.Length..].Trim();
|
||||
|
||||
// token 為空
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"api key auth failed: reason=empty_token path={Path} remote={Remote}",
|
||||
context.Request.Path, context.Connection.RemoteIpAddress);
|
||||
await WriteUnauthorizedAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// constant-time compare — FixedTimeEquals 要求兩邊長度相同,所以先比長度
|
||||
// (長度本身不是 secret,預先 reject 不會洩漏資訊)
|
||||
var tokenBytes = Encoding.UTF8.GetBytes(token);
|
||||
if (tokenBytes.Length != _expectedKeyBytes.Length ||
|
||||
!CryptographicOperations.FixedTimeEquals(tokenBytes, _expectedKeyBytes))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"api key auth failed: reason=token_mismatch path={Path} remote={Remote}",
|
||||
context.Request.Path, context.Connection.RemoteIpAddress);
|
||||
// 注意:log 絕對不印 token 本身
|
||||
await WriteUnauthorizedAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通過 — 放行到 next middleware
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private static async Task WriteUnauthorizedAsync(HttpContext context)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync("{\"error\":\"unauthorized\"}");
|
||||
}
|
||||
}
|
||||
|
||||
// IApplicationBuilder extension — 讓 Program.cs 可以 app.UseApiKeyAuth(...)
|
||||
public static class ApiKeyAuthMiddlewareExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseApiKeyAuth(
|
||||
this IApplicationBuilder builder, string expectedKey)
|
||||
{
|
||||
return builder.UseMiddleware<ApiKeyAuthMiddleware>(expectedKey);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Program.cs 使用範例:
|
||||
|
||||
```csharp
|
||||
// Program.cs(節錄)
|
||||
var expectedKey = builder.Configuration["FAA_API_KEY"]
|
||||
?? Environment.GetEnvironmentVariable("FAA_API_KEY")
|
||||
?? throw new InvalidOperationException("FAA_API_KEY env not set");
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseApiKeyAuth(expectedKey);
|
||||
// 之後才掛 controllers / endpoints
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
**寫法 B:Minimal API Inline Middleware(.NET 8 新 project)**
|
||||
|
||||
```csharp
|
||||
// Program.cs(節錄)
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var expectedKey = builder.Configuration["FAA_API_KEY"]
|
||||
?? Environment.GetEnvironmentVariable("FAA_API_KEY")
|
||||
?? throw new InvalidOperationException("FAA_API_KEY env not set");
|
||||
|
||||
var expectedKeyBytes = Encoding.UTF8.GetBytes(expectedKey);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("ApiKeyAuth");
|
||||
var authHeader = context.Request.Headers.Authorization.ToString();
|
||||
const string prefix = "Bearer ";
|
||||
|
||||
string? failReason = authHeader switch
|
||||
{
|
||||
"" => "missing_authorization_header",
|
||||
var s when !s.StartsWith(prefix, StringComparison.Ordinal) => "missing_bearer_prefix",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (failReason is null)
|
||||
{
|
||||
var token = authHeader[prefix.Length..].Trim();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
failReason = "empty_token";
|
||||
}
|
||||
else
|
||||
{
|
||||
var tokenBytes = Encoding.UTF8.GetBytes(token);
|
||||
if (tokenBytes.Length != expectedKeyBytes.Length ||
|
||||
!CryptographicOperations.FixedTimeEquals(tokenBytes, expectedKeyBytes))
|
||||
{
|
||||
failReason = "token_mismatch";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failReason is not null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"api key auth failed: reason={Reason} path={Path} remote={Remote}",
|
||||
failReason, context.Request.Path, context.Connection.RemoteIpAddress);
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync("{\"error\":\"unauthorized\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.MapGet("/api/v1/files/{key}", (string key) => Results.Ok(/* ... */));
|
||||
app.Run();
|
||||
```
|
||||
|
||||
#### 3.5.3 兩端共通的部署檢查清單
|
||||
|
||||
不分 Go / C#,部署前必須逐項確認:
|
||||
不分 client 端 / server 端,部署前 converter 兩側必須逐項確認:
|
||||
|
||||
| # | 檢查項 | 為什麼 |
|
||||
|---|--------|--------|
|
||||
| 1 | env 已設定且非空(啟動 fail-fast)| 避免「未設定 = 全部放行」災難;server 應在啟動時 panic / throw、不要等到第一個 request 才發現 |
|
||||
| 2 | constant-time compare(Go `subtle.ConstantTimeCompare` / C# `CryptographicOperations.FixedTimeEquals`)| 避免 timing attack 反推 key |
|
||||
| 2 | constant-time compare(Go `subtle.ConstantTimeCompare`)| 避免 timing attack 反推 key |
|
||||
| 3 | 401 response body 統一(不洩漏「key 對 / 不對」/ 「missing / mismatch」差異)| 對外只回 `{"error":"unauthorized"}`,差異只記在 server 端 log |
|
||||
| 4 | log 絕對不印 token 本身 | 即使是失敗的 token 也不印(攻擊者可能用半正確的 token 試探);只印 reason / path / remote |
|
||||
| 5 | Bearer prefix 嚴格驗證(缺 prefix 也 401)| 不允許 `Authorization: <token>` 這種格式(即使內容對也 reject、強制 client 用標準 Bearer scheme) |
|
||||
@ -441,43 +337,58 @@ app.Run();
|
||||
| 7 | key 不進 git | `.gitignore` 嚴格 ignore `.env*`;CI / CD secret 從 Secrets Manager / Vault 注入 |
|
||||
| 8 | 健康檢查 endpoint 是否要 bypass auth | 通常 `/healthz` / `/readyz` 應 bypass(讓 LB / k8s 可探測),但業務 endpoint 全部要 auth |
|
||||
|
||||
### 4. 不再有 scope 概念
|
||||
> **FAA 端不需要本清單**——v2.0 起 FAA 端使用既有 OAuth + delegated token 機制,無新增 API key middleware。FAA 端的 OAuth / delegated token 部署檢查請參考 ADR-014 §2 與 `conversion.md` §3.2。
|
||||
|
||||
OAuth `client_credentials` 設計中四個 scope(`converter:job.write/read`、`files:download.read/delegate`)取消:
|
||||
### 4. 不再有 scope 概念(converter 線適用;FAA 線 v2.0 撤回)
|
||||
|
||||
- 單一 API key 就是「visionA 有權打 converter」/ 「visionA 有權打 FAA」的完整證明
|
||||
**converter 線**:OAuth `client_credentials` 設計中兩個 converter scope(`converter:job.write/read`)取消。
|
||||
|
||||
- 單一 API key 就是「visionA 有權打 converter」的完整證明
|
||||
- 不再有「同一個 client 但拿不同 scope」的細粒度區分(在 1:1 trust 中本來就沒意義)
|
||||
- converter / FAA 端 middleware 也不需要驗 scope
|
||||
- converter 端 middleware 也不需要驗 scope
|
||||
|
||||
### 5. 不再有 tenant 概念
|
||||
**FAA 線**:v1.x 取消 `files:upload.write / files:metadata.read / files:delete / files:download.delegate` 4 個 scope 的決策**撤回**。FAA 4 個 scope 全部恢復、由 MC service client `4242ba63...` 持有,並由 FAA `EnsureJwtScopeAndTenant` 驗。
|
||||
|
||||
visionA → converter / FAA 不再帶 tenant_id:
|
||||
### 5. tenant 概念
|
||||
|
||||
- converter 端的 user_id 從 multipart body `user_id` field 拿(仍由 visionA 從 OIDC sub 灌入,這條 trust boundary 不變)
|
||||
- FAA 端也不需要 tenant 概念(pull NEF 用 object_key 定位)
|
||||
- `VISIONA_OIDC_TENANT_ID` env 在 conversion 場景**廢棄**(如果其他場景還有用就保留,目前未發現其他依賴)
|
||||
**converter 線**:visionA → converter 不再帶 tenant_id。converter 端的 user_id 從 multipart body `user_id` field 拿(仍由 visionA 從 OIDC sub 灌入,這條 trust boundary 不變)。converter 端不需要 tenant 概念。
|
||||
|
||||
### 6. visionA backend 移除的程式碼
|
||||
**FAA 線(v2.0 修正 — 從 v1.x「不再有 tenant」改為「FAA 線需要 tenant」)**:
|
||||
|
||||
| 項目 | 處理 |
|
||||
|------|------|
|
||||
| `internal/conversion/mc_token_client.go`(整個 package) | **整個檔案刪除**(~440 行 — token cache + delegated download token + double-checked locking)|
|
||||
| `internal/conversion/converter_client.go` 內呼叫 `MCTokenClient.ServiceToken()` 的地方 | 改成讀 `cfg.Conversion.ConverterAPIKey` 直接 set header |
|
||||
| `internal/conversion/faa_client.go` 內呼叫 `MCTokenClient.ServiceToken()` 的地方 | 改成讀 `cfg.Conversion.FAAAPIKey` 直接 set header |
|
||||
| `internal/conversion/flow.go` 內呼叫 `mc.IssueDelegatedDownload()` 的地方 | **delegated download token 路徑取消**(見下方 §7) |
|
||||
| `internal/config/config.go` 內 `OIDCConfig.ServiceClientID` / `ServiceClientSecret` 兩個欄位 | **欄位廢棄**(保留欄位以保持 backward compat,但 conversion 已不依賴);env `VISIONA_OIDC_SERVICE_CLIENT_ID` / `SECRET` 從 `.env*.example` 移除 |
|
||||
| `internal/config/config.go` 內 `ConversionConfig.TenantID` 欄位 + env `VISIONA_OIDC_TENANT_ID` | conversion 模組不再依賴;如其他模組未使用即可移除 |
|
||||
- FAA 端 `EnsureJwtScopeAndTenant` 函式會驗 service token 內的 `tenant_id` claim 等於 `instanceOptions.TenantId`(見 FAA `Program.cs` line 303-312)
|
||||
- delegated download token 路徑也驗 `validationResult.TenantId.Value != instanceOptions.TenantId`(line 218-221)
|
||||
- → visionA 的 service token / delegated download token 必須含正確的 `tenant_id` claim
|
||||
- 來源:MC service client `4242ba63...` 註冊時對應的 tenant,stage 為 `732270c0-449c-489c-bfad-321e9bf89b3d`
|
||||
- → `VISIONA_OIDC_TENANT_ID` env **重新啟用**(v1.x 標廢棄;v2.0 撤回廢棄)
|
||||
|
||||
新增的 config 欄位:
|
||||
### 6. visionA backend 移除的程式碼(v1.x 移除清單,v2.0 部分復活)
|
||||
|
||||
| 項目 | v1.x 處理 | **v2.0 處理** |
|
||||
|------|-----------|---------------|
|
||||
| `internal/conversion/mc_token_client.go`(整個 package) | 整個檔案刪除(~440 行) | **部分復活** — 保留 service token cache + delegated download token issue 邏輯(給 FAA 用),但不再被 converter_client 引用 |
|
||||
| `internal/conversion/converter_client.go` 內呼叫 `MCTokenClient.ServiceToken()` | 改成讀 `cfg.Conversion.ConverterAPIKey` 直接 set header | **同 v1.x**(不變)—— converter 線維持 API key |
|
||||
| `internal/conversion/faa_client.go` 內呼叫 `MCTokenClient.ServiceToken()` | 改成讀 `cfg.Conversion.FAAAPIKey` 直接 set header | **撤回**——回到 v1.x 之前:呼叫 `MCTokenClient.ServiceToken()` 拿 service token、帶 `Authorization: Bearer <service-token>`;download 路徑額外呼叫 `MCTokenClient.IssueDelegatedDownload(...)` |
|
||||
| `internal/conversion/flow.go` 內呼叫 `mc.IssueDelegatedDownload()` | 「delegated download token 路徑取消」 | **撤回**——download stream proxy 路徑要呼叫 `IssueDelegatedDownload(...)` 拿 token、再呼叫 `faa.DownloadWithDelegated(token, objectKey)` |
|
||||
| `internal/config/config.go` 內 `OIDCConfig.ServiceClientID` / `ServiceClientSecret` 兩個欄位 | 廢棄(保留 struct field 為 backward compat、不再使用);env `VISIONA_OIDC_SERVICE_CLIENT_ID` / `SECRET` 從 `.env*.example` 移除 | **重新啟用**——FAA 線需要;env 加回 `.env.stage.example` |
|
||||
| `internal/config/config.go` 內 `ConversionConfig.TenantID` 欄位 + env `VISIONA_OIDC_TENANT_ID` | conversion 模組不再依賴;如其他模組未使用即可移除 | **重新啟用**——FAA 端 `EnsureJwtScopeAndTenant` 驗 tenant_id claim、token 必須帶 |
|
||||
|
||||
config 欄位 v2.0 樣貌:
|
||||
|
||||
```go
|
||||
// internal/config/config.go
|
||||
type ConversionConfig struct {
|
||||
ConverterBaseURL string // 既有
|
||||
FAABaseURL string // 既有
|
||||
ConverterAPIKey string // 新增 — env VISIONA_CONVERTER_API_KEY
|
||||
FAAAPIKey string // 新增 — env VISIONA_FAA_API_KEY
|
||||
// TenantID 廢棄
|
||||
ConverterAPIKey string // v1.0 新增 — env VISIONA_CONVERTER_API_KEY(v2.0 維持)
|
||||
// FAAAPIKey string // v1.0 加的;v2.0 撤回(不再需要)
|
||||
TenantID string // v1.x 廢棄;v2.0 重新啟用 — env VISIONA_OIDC_TENANT_ID
|
||||
}
|
||||
|
||||
// OIDCConfig 維持有 ServiceClientID / ServiceClientSecret 兩欄位(v2.0 重新啟用)
|
||||
type OIDCConfig struct {
|
||||
// ... user login 相關欄位(不變)...
|
||||
ServiceClientID string // v2.0 重新啟用 — env VISIONA_OIDC_SERVICE_CLIENT_ID
|
||||
ServiceClientSecret string // v2.0 重新啟用 — env VISIONA_OIDC_SERVICE_CLIENT_SECRET
|
||||
}
|
||||
|
||||
// Enabled 改判定:
|
||||
@ -485,39 +396,59 @@ func (c ConversionConfig) Enabled() bool {
|
||||
return c.ConverterBaseURL != "" &&
|
||||
c.FAABaseURL != "" &&
|
||||
c.ConverterAPIKey != "" &&
|
||||
c.FAAAPIKey != ""
|
||||
// FAA 線判定 OIDCConfig 有 ServiceClientID / Secret + ConversionConfig.TenantID
|
||||
// — 由 main.go 啟動時組合判斷,這裡只判 conversion 自己的欄位
|
||||
c.TenantID != ""
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Delegated download token 路徑的處理(重要 — Phase 0.8b 範圍說明)
|
||||
### 7. Delegated download token 路徑的處理(v2.0 撤回 v1.x 的撤回,回到 ADR-014 §2 設計)
|
||||
|
||||
ADR-014 §2 設計 download 流程是:
|
||||
ADR-014 §2 原設計 download 流程:
|
||||
|
||||
```
|
||||
browser → visionA /download → MC issue delegated token → 302 → browser → FAA?access_token=...
|
||||
```
|
||||
|
||||
API key 模式下「MC issue delegated token」這條鏈不存在了。Phase 0.8b 對 download 路徑的處理:
|
||||
**v1.x 的選項 A(短期 server-side proxy with API key)撤回**:v1.0 / v1.1 在本節原本提案「visionA backend 直接用 `Authorization: Bearer <FAA_API_KEY>` 拉 FAA、stream 回 browser」整段全部撤回。
|
||||
|
||||
**選項 A(Phase 0.8b 採用)— 短期:保持 server-side download proxy**
|
||||
|
||||
visionA backend 直接用 `Authorization: Bearer <FAA_API_KEY>` 拉 FAA,stream 回 browser:
|
||||
**v2.0 採用的設計(保留 server-side stream proxy 不退回 302、但 token 來源改回 delegated download token)**:
|
||||
|
||||
```
|
||||
browser → visionA /download → visionA backend
|
||||
↓
|
||||
Bearer <FAA_API_KEY>
|
||||
ownership 檢查
|
||||
↓
|
||||
FAA
|
||||
ensurePromoted(拿 target_object_key)
|
||||
↓
|
||||
stream NEF 回 browser
|
||||
MCTokenClient.ServiceToken() → MC service access_token
|
||||
↓
|
||||
MCTokenClient.IssueDelegatedDownload(token, object_key, "GET", 5min)
|
||||
→ MC POST /file-access/download-tokens
|
||||
↓
|
||||
FAA GET /files/{key}
|
||||
Authorization: Bearer <delegated-token>
|
||||
↓
|
||||
FAA IDelegatedDownloadTokenValidator.ValidateAsync(...)
|
||||
↓
|
||||
FAA stream NEF binary
|
||||
↓
|
||||
visionA backend io.CopyN → browser
|
||||
```
|
||||
|
||||
- 跨 internet 流量 N 倍是接受的取捨(Phase 0.8 MVP user 量小、單檔 NEF 通常 < 50MB)
|
||||
- token 結構性不過 frontend JS(仍維持 ADR-014 §2 表格的「server-side 比 frontend 拿 token 更安全」的所有優點)
|
||||
- visionA backend 變成 streaming bottleneck,Phase 1 量大時再評估
|
||||
**為什麼保留 server-side stream proxy(不退回 ADR-014 §2 的 302 redirect)**:
|
||||
|
||||
**選項 B(Phase 1+ 升級路徑)— FAA 上 delegated token 機制改用「visionA 自己簽 short-TTL HMAC token」**
|
||||
- T4 已經在 Phase 0.8 把 download 改成 server-side stream proxy(`conversion.md` v0.4 / `api/api-conversion.md` v0.4),實測 frontend `<a href download>` 流程已驗證
|
||||
- 退回 302 redirect 等於 frontend 行為改變、要重做 e2e 驗證、無收益
|
||||
- delegated token 在 server-side(不洩漏給 frontend JS / browser URL bar)反而比 302 模式更安全
|
||||
- 流量成本(每次下載繞 visionA backend N×)Phase 0.8 MVP 量小可接受;Phase 1 量大時再評估升級
|
||||
|
||||
**為什麼 token 來源改回 delegated download token(不繼續用 v1.x 的 visionA API key)**:
|
||||
|
||||
- FAA `GET /files/{key}` endpoint 強制使用 delegated token(line 184-254 沒掛 `RequireAuthorization()`、改用 `IDelegatedDownloadTokenValidator`),FAA 端不接受其他 token type
|
||||
- v1.x 若要用 visionA API key,需要 warrenchen 在 FAA 端為 download endpoint 額外實作「API key middleware」並改寫 dual-auth 路由——成本遠超 v2.0 撤回的收益
|
||||
|
||||
**選項 B(Phase 1+ HMAC token 升級路徑)保留為 follow-up**:
|
||||
|
||||
如果 Phase 1 流量壓力大要回 302 redirect 模式,visionA 可以自己簽 short-TTL HMAC token(不需要 MC 介入),FAA 端 middleware 多加一條「驗 visionA HMAC token」的路徑:
|
||||
|
||||
@ -528,41 +459,50 @@ browser → visionA /download → visionA 用 HMAC_KEY 簽 short-TTL token
|
||||
↓
|
||||
browser → FAA?access_token=<visionA-signed-hmac>
|
||||
↓
|
||||
FAA middleware:API key (server-to-server) OR HMAC (browser direct) 二選一
|
||||
FAA middleware:JWT (s2s) OR delegated (current) OR HMAC (browser direct) 三選一
|
||||
```
|
||||
|
||||
此升級路徑與本 ADR 的決策無衝突,記入 Phase 1 follow-up。
|
||||
此升級路徑與本 ADR v2.0 決策無衝突,記入 Phase 1 follow-up(同 v1.x 規劃)。
|
||||
|
||||
### 8. user_id 注入 trust boundary 不變
|
||||
### 8. user_id 注入 trust boundary 不變(v1.x / v2.0 一致)
|
||||
|
||||
ADR-014 §6「visionA backend 是唯一灌 user_id 的點」邏輯不變:
|
||||
|
||||
- user_id 仍從 OIDC cookie session 拿(OIDC sub)
|
||||
- 仍透過 multipart streaming 注入 converter request 的 `user_id` field(converter 端視 visionA 為 trusted caller)
|
||||
- API key 證明的是「caller 是 visionA」,user_id 的真實性由 visionA 內部的 OIDC 機制保證 — 兩條獨立鏈
|
||||
- API key(converter)/ service token(FAA)證明的是「caller 是 visionA」,user_id 的真實性由 visionA 內部的 OIDC 機制保證 — 兩條獨立鏈
|
||||
|
||||
### 9. 部署層的 env 注入
|
||||
### 9. 部署層的 env 注入(v2.1 修訂)
|
||||
|
||||
Phase 0.8b 採用:
|
||||
Phase 0.8b v2.1 採用(visionA 端 server-to-server secret 只剩 converter API key 一把):
|
||||
|
||||
| Env | Stage | Production |
|
||||
|-----|-------|-----------|
|
||||
| `VISIONA_CONVERTER_API_KEY`(visionA 端) | `.env.stage`(jimchen 持有,不進 git) | AWS Secrets Manager / Vault |
|
||||
| `CONVERTER_API_KEY`(converter 端) | `.env`(jimchen 持有,不進 git) | 同上 |
|
||||
| `VISIONA_FAA_API_KEY`(visionA 端) | `.env.stage` | 同上 |
|
||||
| `FAA_API_KEY`(FAA 端) | warrenchen 設置 | 同上 |
|
||||
| Env | Stage | Production | v2.1 變更 |
|
||||
|-----|-------|-----------|----------|
|
||||
| `VISIONA_CONVERTER_API_KEY`(visionA 端) | `.env.stage`(jimchen 持有,不進 git) | AWS Secrets Manager / Vault | 維持(v1.0 新增、v2.0 維持、v2.1 維持) |
|
||||
| `CONVERTER_API_KEY`(converter 端) | `.env`(jimchen 持有,不進 git) | 同上 | 維持 |
|
||||
| `VISIONA_CONVERTER_BASE_URL` | `.env.stage` | Secrets Manager | 維持(既有) |
|
||||
| ~~`VISIONA_FAA_API_KEY`(visionA 端)~~ | — | — | **撤回**(v1.0 加的;v2.0 移除;v2.1 維持移除) |
|
||||
| ~~`FAA_API_KEY`(FAA 端)~~ | — | — | **撤回**(v1.0 加的;v2.0 移除;v2.1 維持移除)|
|
||||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_ID`~~ | — | — | **v2.1 再次撤回**(v1.x 廢棄;v2.0 重新啟用;v2.1 撤回 v2.0 啟用)|
|
||||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_SECRET`~~ | — | — | **v2.1 再次撤回**(同上) |
|
||||
| ~~`VISIONA_OIDC_TENANT_ID`~~ | — | — | **v2.1 再次撤回**(同上) |
|
||||
| ~~`VISIONA_FAA_BASE_URL`~~ | — | — | **v2.1 撤回**(visionA 端不再直接打 FAA、走 converter 中轉)|
|
||||
|
||||
key 產生方式:`openssl rand -hex 32`(64 字元 hex)
|
||||
key 產生方式:
|
||||
- converter API key(兩端對齊):`openssl rand -hex 32`(64 字元 hex)
|
||||
- ~~service client secret~~(v2.1 不需要)
|
||||
|
||||
**v2.1 後 visionA 端 server-to-server 鏈路收斂為單條**:visionA → converter(API key),download 也走同一條(converter `GET /api/v1/jobs/{id}/result`,詳見 ADR-016)。
|
||||
|
||||
## 考慮過的替代方案 (Alternatives Considered)
|
||||
|
||||
### 方案 A:維持 OAuth client_credentials(ADR-014 原方案)
|
||||
### 方案 A:維持 OAuth client_credentials(ADR-014 原方案 — converter / FAA 兩條都走)
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | 標準化、跨團隊可重用、短 TTL token、scope 細粒度可控 |
|
||||
| 缺點 | 需要 MC team 配合 onboard scope、converter / FAA 都要重寫 middleware、JWKS 取得失敗時 graceful degrade 複雜、stage e2e 鏈路 4 個 blocker 全要修齊 |
|
||||
| 排除原因 | 對 1:1 internal trust 場景過度設計;Phase 0.8 stage 部署實際遇到的 4 個 blocker 證明這條路 ROI 不好 |
|
||||
| 缺點 | 需要 MC team 配合 onboard converter scope、converter / FAA 都要重寫 middleware、JWKS 取得失敗時 graceful degrade 複雜、stage e2e 鏈路 4 個 blocker 全要修齊 |
|
||||
| 排除原因 | 對 1:1 internal trust 場景(converter)過度設計;Phase 0.8 stage 部署實際遇到的 4 個 blocker 證明這條路 ROI 不好 |
|
||||
|
||||
### 方案 B:mTLS(mutual TLS)
|
||||
|
||||
@ -586,72 +526,103 @@ key 產生方式:`openssl rand -hex 32`(64 字元 hex)
|
||||
|------|------|
|
||||
| 優點 | env 少一個、部署設定簡單 |
|
||||
| 缺點 | 一處洩漏兩處連坐;converter rotate 必須同步 FAA;違反「每條 trust boundary 各自獨立」原則 |
|
||||
| 排除原因 | 每個下游各自獨立的 key 是低成本(只多一個 env),但隔離效益高,採方案決策的反方案 |
|
||||
| 排除原因 | 在 v1.x 兩條都 API key 的設計中作為反方案被排除;v2.0 因 FAA 撤回 API key、議題自然不存在 |
|
||||
|
||||
### 方案 E(v2.0 採用):visionA → converter API key、visionA → FAA 仍走 MC service token + delegated download token
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | (1) 不必動 FAA repo(warrenchen 維護成本歸零);(2) 不必動 MC 端 scope onboarding(4242ba63 service client 已備妥 4 scope);(3) FAA 既有 dual-auth 設計(JWT for write / metadata / delete + delegated token for download)零修改;(4) converter 線 v1.0 已得到的「不必動 MC、不必協調」收益**對 converter 而言維持**;(5) FAA 線維持 OAuth 框架對 FAA 多 client 演進更友善(FAA 之後可能服務多個 visionA-like 產品線)|
|
||||
| 缺點 | (1) visionA 仍要保留 mc_token_client 的 service token cache + delegated download token issue 邏輯(v1.x 砍掉的 ~440 行要部分復活);(2) MC 仍是 FAA 線的依賴(MC 掛 → FAA 用不了,但 converter 不受影響);(3) 兩條線的認證機制不對稱(converter API key、FAA OAuth),心智負擔略高 |
|
||||
| 排除 v1.0 方案(兩條都 API key)的原因 | 使用者 2026-05-16 拍板:(1) 不希望動 FAA(共用 repo、warrenchen 維護);(2) 不希望動 MC(5/9 撞 scope 沒註冊的痛);(3) 但 5/16 提供的 service client `4242ba63...` 證明 MC 端針對 FAA 的 4 個 scope 已備妥 — v1.0 拒絕走 OAuth 的「MC scope 沒備妥」前提在 FAA 線**不成立**|
|
||||
| 採用 | **v2.0 採用** |
|
||||
|
||||
## 後果 (Consequences)
|
||||
|
||||
### 正面影響
|
||||
|
||||
- **實作大幅簡化**:visionA backend 砍 `internal/conversion/mc_token_client.go`(~440 行 — 含 token cache + delegated download token + double-checked locking + retry policy)
|
||||
- **不依賴 MC scope onboarding**:MC team 完全不需介入,stage e2e blocker 從 4 個降到 0
|
||||
- **converter / FAA middleware 極簡**:兩端各自只需「比對單一字串 + constant-time compare」,無需驗 JWKS / scope / tenant;converter Phase 1 之前舊 image 可直接補 middleware 上線(不需 redeploy 大改)
|
||||
- **失敗模式收斂**:原本「MC 5xx / MC 4xx / token cache miss / scope mismatch」四個失敗類型,收斂為「API key 對 / 不對」單一布林
|
||||
- **可觀測性減負**:不需追 token cache hit rate、不需追 MC 失敗率
|
||||
- **已洩漏的 stage service client secret 直接作廢**:不需協調 MC team rotate
|
||||
- **converter 線實作大幅簡化**(v1.0 收益保留):visionA backend 對 converter 的呼叫不查 cache、不打 MC、不重簽
|
||||
- **converter / FAA stage e2e blocker 收斂**:converter 線 0 個 blocker;FAA 線靠使用者已備妥的 service client `4242ba63...` 驗證後即可上線(待 verify)
|
||||
- **不必動 FAA repo、不必動 warrenchen**(v2.0 新增收益):v1.x 規劃要 warrenchen 配合改 FAA middleware 的工作完全取消
|
||||
- **不必動 MC scope onboarding**(部分 v2.0 新增收益):FAA 線 4 個 scope 已備妥;converter 線本來就不依賴 MC
|
||||
- **converter middleware 極簡**(v1.0 收益保留):只需「比對單一字串 + constant-time compare」
|
||||
- **converter 失敗模式收斂**:原本「MC 5xx / MC 4xx / token cache miss / scope mismatch」四個失敗類型,收斂為「API key 對 / 不對」單一布林
|
||||
- **可觀測性減負(部分)**:converter 線不需追 token cache hit rate / MC 失敗率;FAA 線仍需追,但範圍縮小一半
|
||||
- **已洩漏的 stage service client secret `RciRUyi...` 直接作廢**:使用者於 v2.0 提供新 client `4242ba63...` 取代
|
||||
|
||||
### 負面影響(接受的取捨)
|
||||
|
||||
- **API key 是 long-lived secret**:不像 OAuth token 有 TTL(通常 1 小時);rotate 需要 visionA + 下游同步換 env 並 redeploy;secret 管理責任更重
|
||||
- **沒有 scope 細粒度**:未來如果 visionA 內部要區分「能 init job 但不能 promote」這種需求,要回頭加(但 1:1 trust 場景幾乎不會有此需求)
|
||||
- **沒有 audit trail(誰用 token 做什麼)**:OAuth + JWT 的 sub claim 提供天然 audit;API key 模式下 converter / FAA 只知道「是 visionA」,需要靠 visionA 內部 log + request_id 串接才能追到 user_id(既有 request_id 機制可滿足)
|
||||
- **delegated download token 暫時不能用 302 redirect 模式**:Phase 0.8b 退回 server-side download proxy;Phase 1 量大時要另外設計(見 §7 選項 B)
|
||||
- **converter 線 API key 是 long-lived secret**:不像 OAuth token 有 TTL(通常 1 小時);rotate 需要 visionA + converter 同步換 env 並 redeploy;secret 管理責任更重
|
||||
- **converter 線沒有 scope 細粒度**:未來如果 visionA 內部要區分「能 init job 但不能 promote」這種需求,要回頭加(但 1:1 trust 場景幾乎不會有此需求)
|
||||
- **converter 線沒有 audit trail(誰用 token 做什麼)**:OAuth + JWT 的 sub claim 提供天然 audit;API key 模式下 converter 只知道「是 visionA」,需要靠 visionA 內部 log + request_id 串接才能追到 user_id(既有 request_id 機制可滿足)
|
||||
- **FAA 線維持 OAuth client_credentials 鏈條的部分複雜度**(v2.0 新增):visionA 仍需 mc_token_client(service token cache + delegated download token issue + retry policy);MC 仍是 FAA 線的單點依賴;FAA 線的可觀測性負擔(token cache hit rate / MC 失敗率)保留
|
||||
- **兩條線認證機制不對稱**(v2.0 新增):converter 線 API key、FAA 線 OAuth;運維 / 排查時需區分兩種失敗模式(converter 401 = key 不同步;FAA 401 = service token 取得失敗 / scope mismatch / tenant mismatch / delegated token 過期)
|
||||
|
||||
### 風險
|
||||
|
||||
| 風險 | 緩解 |
|
||||
|------|------|
|
||||
| API key 洩漏(git log、log shipping、Slack 訊息等)| (1) `.gitignore` 嚴格 ignore `.env*`;(2) log 不印 token;(3) stage / prod 用不同 key;(4) 一旦發現洩漏 → 換 env → redeploy(雙方協調 < 1 小時可完成)|
|
||||
| Rotate 流程缺失 | 配套必須產出 rotate runbook(jimchen 對 converter,warrenchen 對 FAA) |
|
||||
| 兩個 key 管理混淆(converter / FAA) | env 命名嚴格區分(`VISIONA_CONVERTER_API_KEY` / `VISIONA_FAA_API_KEY`);config validate 啟動時檢查兩個都非空 |
|
||||
| 開發環境 / stage / prod 用同一把 key | 嚴格分環境產 key(dev / stage / prod 各自 `openssl rand -hex 32`),不重用 |
|
||||
| converter API key 洩漏(git log、log shipping、Slack 訊息等)| (1) `.gitignore` 嚴格 ignore `.env*`;(2) log 不印 token;(3) stage / prod 用不同 key;(4) 一旦發現洩漏 → 換 env → redeploy(雙方協調 < 1 小時可完成)|
|
||||
| converter API key Rotate 流程缺失 | 配套必須產出 rotate runbook(jimchen 對 converter)|
|
||||
| FAA 線 service client secret 洩漏(v2.0 新增)| 同 v1.x 處理:MC team 換 client secret → visionA 同步 env → restart;運維事件,需跨 MC team 協調 |
|
||||
| MC stage 端 4242ba63 service client 4 個 scope 是否真的有效 / 啟用(v2.0 新增)| **待 verify**:stage redeploy 前實測 `POST /oauth/token` 帶 4 scope 拿到 access_token;若部分 scope 缺失需 MC team 補(合規性段落追蹤)|
|
||||
| FAA 線 MC 不可達 → 無法 issue delegated token → 下載失敗(v2.0 新增)| graceful degradation:對 frontend 回 502 `download_token_failed` / `mc_token_unavailable`(仍維持 ADR-014 §7 原 retry 矩陣)|
|
||||
| 開發環境 / stage / prod 用同一把 converter API key | 嚴格分環境產 key(dev / stage / prod 各自 `openssl rand -hex 32`),不重用 |
|
||||
|
||||
## 合規性
|
||||
|
||||
- [x] 與使用者確認:採 API key + Authorization Bearer + 每個下游獨立 key
|
||||
- [x] 與 jimchen 確認(同時為 visionA + converter 維護者):converter middleware 改寫由 jimchen 處理
|
||||
- [ ] 與 warrenchen 確認:FAA 端 middleware 改寫由 warrenchen 處理(**待 Phase 0.8b 步驟 5**)
|
||||
- [x] 與 ADR-014 對齊:本 ADR 部分 supersede ADR-014 §5 / §6(OAuth 段落) / §7(MC token retry rows),不影響 ADR-014 其他段落
|
||||
- [x] 與使用者確認:v2.0 範圍縮限至 visionA → converter API key;visionA → FAA 回到 ADR-014 §2 原設計(2026-05-16)
|
||||
- [x] 與 jimchen 確認(同時為 visionA + converter 維護者):converter middleware 改寫由 jimchen 處理(沿用 v1.0 步驟 4 規劃)
|
||||
- [x] ~~與 warrenchen 確認:FAA 端 middleware 改寫由 warrenchen 處理~~(**v2.0 撤回此項;FAA repo 不需改動**)
|
||||
- [ ] **與 MC team 驗證 stage 端 4242ba63 service client 4 個 scope 都可用**(v2.0 新增;step 6 stage redeploy 前必驗):
|
||||
- `files:upload.write` ✅ 對應 FAA `PUT /files/...`
|
||||
- `files:metadata.read` ✅ 對應 FAA `GET /files/metadata/...` + `HEAD /files/...`
|
||||
- `files:delete` ✅ 對應 FAA `DELETE /files/...`
|
||||
- `files:download.delegate` ✅ 用來向 MC `POST /file-access/download-tokens` 換 delegated download token,再打 FAA `GET /files/{key}`
|
||||
- [x] 與 ADR-014 對齊(v2.0 修訂):本 ADR v2.0 起對 ADR-014 的 supersede 範圍縮限至「§5 中 converter 部分 service token / `converter:job.write/read` scope」;ADR-014 §2 / §5 中 FAA 部分 / §6 / §7 中 FAA 與 MC delegated token row 全部恢復有效
|
||||
- [x] 與 ADR-013 對齊:本 ADR 不影響 user login 的 public PKCE client
|
||||
- [ ] DevOps rotate runbook 待產出(Phase 0.9 follow-up)
|
||||
- [ ] 已洩漏的 stage service client secret `RciRUyi...` 自動作廢(不需 MC rotate)
|
||||
- [ ] DevOps rotate runbook 待產出(Phase 0.9 follow-up;範圍縮限至 converter API key)
|
||||
- [ ] 已洩漏的 stage service client secret `RciRUyi...` 自動作廢(不需 MC rotate;v2.0 由新 client `4242ba63...` 取代)
|
||||
|
||||
## 配套產出(給後續 Phase)
|
||||
|
||||
### Phase 0.8b 範圍內
|
||||
### Phase 0.8b 範圍內(v2.0 修訂)
|
||||
|
||||
- visionA backend 程式碼改造(backend agent 任務)
|
||||
- converter middleware 改造(jimchen 跨 repo)
|
||||
- FAA middleware 改造(warrenchen 跨 repo)
|
||||
- `.env.stage.example` 更新(移除 service client env、新增 API key env)
|
||||
- 設計文件更新(conversion.md / api-conversion.md / oidc-tdd.md — 本 ADR 同步產出)
|
||||
- visionA backend 程式碼改造(backend agent 任務):
|
||||
- converter_client.go 維持 v1.0 改造(API key)
|
||||
- **faa_client.go 改回呼叫 MCTokenClient.ServiceToken() + 為 download 路徑增 DownloadWithDelegated 變體**
|
||||
- **mc_token_client.go 部分復活**(service token cache + delegated download token issue 邏輯)
|
||||
- flow.go download 路徑改回呼叫 IssueDelegatedDownload + DownloadWithDelegated
|
||||
- config.go 重新啟用 OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID
|
||||
- converter middleware 改造(jimchen 跨 repo) — 維持 v1.0 規劃
|
||||
- ~~FAA middleware 改造(warrenchen 跨 repo)~~ — **v2.0 撤回**
|
||||
- `.env.stage.example` 更新(v2.0 修訂):
|
||||
- 維持 `VISIONA_CONVERTER_API_KEY` 新增
|
||||
- 撤回 `VISIONA_FAA_API_KEY`
|
||||
- 加回 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `VISIONA_OIDC_SERVICE_CLIENT_SECRET` / `VISIONA_OIDC_TENANT_ID` / `VISIONA_FAA_BASE_URL`
|
||||
- **這部分 .env*.example 更新由 backend agent 下次任務(複活 mc_token_client 時)一併處理;本次架構任務範圍只動共享文件**
|
||||
- 設計文件更新(conversion.md / api-conversion.md / oidc-tdd.md — 本 ADR v2.0 同步產出)
|
||||
|
||||
### Phase 0.9 / Phase 1 follow-up
|
||||
|
||||
- [ ] API key rotate runbook(每個下游一份,含「產新 key → 雙方同步 env → restart → 驗證 → 拔舊 key」步驟)
|
||||
- [ ] 是否設 API key 有效期(例如 1 年到期自動提醒)— 由 SRE 流程決定
|
||||
- [ ] FAA 上「visionA 自己簽 HMAC token」delegated 機制(§7 選項 B),用於 download 路徑回 302 redirect
|
||||
- [ ] 觀察 server-side download proxy 在 Phase 1 量大時的效能 / 頻寬 cost
|
||||
- [ ] converter API key rotate runbook(含「產新 key → 雙方同步 env → restart → 驗證 → 拔舊 key」步驟;範圍縮限至 converter)
|
||||
- [ ] 是否設 converter API key 有效期(例如 1 年到期自動提醒)— 由 SRE 流程決定
|
||||
- [ ] FAA 上「visionA 自己簽 HMAC token」delegated 機制(§7 選項 B),用於 download 路徑回 302 redirect(同 v1.x 規劃)
|
||||
- [ ] 觀察 server-side download proxy 在 Phase 1 量大時的效能 / 頻寬 cost(同 v1.x 規劃)
|
||||
|
||||
## 相關文件
|
||||
|
||||
- 部分 supersedes:[`adr-014-conversion-integration.md`](./adr-014-conversion-integration.md)(§5 / §6 OAuth 段落 / §7 MC rows)
|
||||
- 部分 supersedes:[`adr-014-conversion-integration.md`](./adr-014-conversion-integration.md)(**v2.0 範圍縮限**:僅 §5 中 converter 部分 service token / scope;§2 / §5 中 FAA 部分 / §6 / §7 中 FAA + MC delegated token row 全部恢復有效)
|
||||
- 不影響:[`adr-013-public-client.md`](./adr-013-public-client.md)(user login 部分)
|
||||
- 詳細實作(本 ADR 同步更新):`conversion.md`、`api/api-conversion.md`、`oidc-tdd.md`
|
||||
- 觸發背景:`progress.md` 「Phase 0.8b 啟動原因(2026-05-11)」段落
|
||||
- 詳細實作(本 ADR v2.0 同步更新):`conversion.md` v0.5、`api/api-conversion.md` v0.5、`oidc-tdd.md` v0.3
|
||||
- 觸發背景:`progress.md` 「Phase 0.8b 啟動原因(2026-05-11)」+ 「Phase 0.8b 步驟 2 — Backend agent 任務範圍盤點」+ 「v2.0 範圍縮限(2026-05-16)」段落
|
||||
- FAA dual-auth 設計參考:`/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` line 41-57(JWT Bearer 設定)、line 184-254(download endpoint 不掛 RequireAuthorization、用 IDelegatedDownloadTokenValidator)、line 291-322(EnsureJwtScopeAndTenant + HasScope)
|
||||
|
||||
## 版本記錄
|
||||
|
||||
| 日期 | 版本 | 變更 |
|
||||
|------|------|------|
|
||||
| 2026-05-11 | 1.0 | 初版 — Phase 0.8b 將 server-to-server 認證從 OAuth client_credentials 改 pre-shared API key |
|
||||
| 2026-05-11 | 1.0 | 初版 — Phase 0.8b 將 server-to-server 認證從 OAuth client_credentials 改 pre-shared API key(visionA → converter / FAA 兩條線都改)|
|
||||
| 2026-05-15 | 1.1 | 補 §3.5 Reference Middleware Implementation — Go (converter) + C# (FAA) snippet + 部署檢查清單,給跨 repo 改 middleware 時照抄 |
|
||||
| 2026-05-16 | 2.0 | **範圍縮限** — 撤回 visionA → FAA API key 部分(FAA 不動、改回 ADR-014 §2 MC service token + delegated download token);visionA → converter API key 路線保留。撤回原因:(1) 使用者明示不希望動 FAA / MC、(2) 5/9 撞 MC scope 沒註冊的痛尚在、但 (3) 使用者今天提供 FAA stage service client (`4242ba63099d4f318dd3f143d27ef4c5`) 證明 MC 端 4 個 scope 都已備好(含 `files:download.delegate`)、舊路線可走通。涉及修訂:§2 整段標撤回 + 加 v2.0 設計(FAA dual-auth + delegated token download)、§3.5.2 C# FAA snippet 整段刪除、§5 tenant 概念分 converter/FAA 兩段、§6 mc_token_client 改部分復活 + OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID 重新啟用、§7 download 路徑改回 delegated token(保留 server-side proxy 不退回 302)、§9 env 表加回 service client + tenant id + 撤回 FAA API key、新增方案 E 排除 v1.0 兩條都 API key 路線、後果重估、合規性「跟 warrenchen 確認」改為「跟 MC team 驗 4 scope」。對 commit `86b7175` 影響:visionA backend faa_client.go 要從 API key 改回 service token + delegated token(待下次 backend agent 處理);mc_token_client.go 要部分復活(保留 ServiceToken cache + IssueDelegatedDownload 邏輯);config.go 加回 ServiceClientID/Secret/TenantID 三欄位;`.env.stage.example` 加回對應 env、撤回 `VISIONA_FAA_API_KEY`。本次純文件修訂、source code 改造留給 backend agent 下次任務。 |
|
||||
| 2026-05-16 | 2.1 | **§2 visionA → FAA 整段再次撤回** — 對 MC source 完整驗證後發現:(1) MC source 沒有 `POST /file-access/download-tokens` endpoint(visionA 無法跟 MC 換 delegated token)、(2) MC source 沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint(即使有 token 也無法 validate)、(3) FAA `GET /files/{key}` 強制只接 delegated token、不接 service token。→ ADR-014 §2 與本 ADR v2.0 §2 描述的「visionA → MC → FAA delegated token 鏈」**完全是 fictional**(從 2026-05-02 寫定起即為 broken design、未曾 e2e 跑通過)。**v2.1 採用 [ADR-016](./adr-016-download-via-converter.md)**(visionA download 改走 converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉)。涉及修訂:狀態行加 v2.1 撤回說明、上位 ADR 區註明 §2 被 ADR-016 supersede、§2 整段標 v2.1 撤回(v2.0 內容保留歷史)、§9 env 表撤回 v2.0 加回的 `VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET` / `VISIONA_OIDC_TENANT_ID` / `VISIONA_FAA_BASE_URL`、改寫「v2.1 visionA 端 server-to-server 鏈路收斂為單條」。對 commit `86b7175` 影響:visionA backend mc_token_client.go **維持砍除狀態**(撤回 v2.0 「部分復活」規劃)、faa_client.go 改名為 converter_result_client.go(或併入 converter_client.go)、config.go 不需加回 ServiceClient* / TenantID / FAABaseURL、`.env.stage.example` 維持只有 converter env。本次純文件修訂、source code 改造留給 backend agent 下次任務(範圍含「converter 跨 repo 新增 GET result endpoint」)。**§1 visionA → converter API key(v1.0 / v2.0 / v2.1 都維持不變)**。 |
|
||||
|
||||
@ -0,0 +1,482 @@
|
||||
# ADR-016:visionA download NEF 改走 converter `GET /api/v1/jobs/{id}/result` 中轉(撤回 FAA 鏈)
|
||||
|
||||
## 狀態
|
||||
|
||||
Accepted — 2026-05-16
|
||||
|
||||
## 上位 / 同層 ADR
|
||||
|
||||
- **部分 supersedes**:
|
||||
- [ADR-014](./adr-014-conversion-integration.md) §2「Download — FAA delegated token」整段(visionA → FAA delegated token 鏈撤回;visionA download 改走 converter `GET /api/v1/jobs/{id}/result`)。ADR-014 §2 中關於「server-side proxy / 不退回 302 / `<a href download>` frontend pattern」的決策**沿用、由本 ADR 接手**。
|
||||
- [ADR-014](./adr-014-conversion-integration.md) §3「半自動 — 加到模型庫」中「visionA backend 跟 FAA pull NEF(scope `files:download.read`)」的部分:本 Phase 0.8 範圍內 visionA 不主動 pull NEF;如 Phase 1+ 需要 server-side pull,改走 converter `GET /api/v1/jobs/{id}/result` 同一條路徑。
|
||||
- [ADR-014](./adr-014-conversion-integration.md) §5「Service token cache — 仿 converter scheduler 模式」中 **FAA 部分**(scope `files:download.read / delegate` 不再由 visionA 取;對應 service client / tenant_id env 撤回)。
|
||||
- [ADR-014](./adr-014-conversion-integration.md) §7「失敗模式 retry 矩陣」中「FAA pull(加到模型庫)」、「MC token endpoint」、「MC delegated token」三 row(visionA 端不再觸發;converter 端有自己的 retry 矩陣,不在本 ADR 範圍)。
|
||||
- [ADR-015 v2.0](./adr-015-server-to-server-api-key.md) §2「visionA → FAA(MC service token + delegated download token)」整段(v2.0 於 §2 維持 ADR-014 §2 設計,本 ADR-016 於 §2 撤回;visionA 端 mc_token_client 整檔再次砍除,OIDC ServiceClient* / TenantID env 廢棄)。
|
||||
|
||||
- **沿用**(不受本 ADR 影響):
|
||||
- [ADR-013](./adr-013-public-client.md):user login 的 OIDC public PKCE client(與 server-to-server 完全解耦)
|
||||
- [ADR-014](./adr-014-conversion-integration.md) §1(upload streaming proxy)、§3「加到模型庫」走 visionA `/api/models/init+finalize`、§4 模組劃分(不擴 model schema、`internal/conversion/` 包裝)、§6 user_id trust boundary
|
||||
- [ADR-015 v2.0](./adr-015-server-to-server-api-key.md) §1「visionA → converter 採 pre-shared API key」(本 ADR 在 download 路徑沿用同一條認證機制)
|
||||
|
||||
- **同層**:本 ADR 與 ADR-014 / ADR-015 v2.0 三者並存後的最終態為:
|
||||
- visionA → converter(init / poll / promote / **result download**):API key(ADR-015 §1)
|
||||
- converter → FAA(promote 內部 PUT NEF):converter 自己用 OAuth client_credentials + scope `files:upload.write`(converter 既有實作,與 visionA 無關)
|
||||
- visionA ↔ FAA:**Phase 0.8b 範圍內不存在這條鏈**(本 ADR 撤回)
|
||||
- visionA ↔ MC(server-to-server):**Phase 0.8b 範圍內不存在這條鏈**(本 ADR 撤回;只有 user login 的 OIDC 公開 client 仍接 MC)
|
||||
|
||||
---
|
||||
|
||||
## 背景 (Context)
|
||||
|
||||
### 起因:致命發現(2026-05-16)
|
||||
|
||||
2026-05-16 在 backend agent 準備依 ADR-015 v2.0 §6「mc_token_client 部分復活」實作之前,對 Innovedus Member Center(MC)與 File Access Agent(FAA)的 source code 做全面驗證,發現 **ADR-014 §2 設計的 delegated download token 鏈路從 2026-05-02 寫文件至今一直是斷的、只是因為 visionA 從未實際 e2e 跑通 download 而沒人發現**。
|
||||
|
||||
#### 致命發現 1:MC 沒有「issue delegated download token」endpoint
|
||||
|
||||
對 `/Users/jimchen/member_center/src/MemberCenter.Api/Controllers/` 完整 grep(8 個 controller):
|
||||
|
||||
```
|
||||
AdminNewsletterListsController.cs AdminOAuthClientsController.cs AdminTenantsController.cs
|
||||
AuthController.cs NewsletterController.cs OAuthController.cs
|
||||
SendEngineIntegrationController.cs SubscriptionsController.cs TokenController.cs
|
||||
UserController.cs
|
||||
```
|
||||
|
||||
全部 endpoint 清單:
|
||||
- `POST /oauth/token`, `POST /oauth/authorize`(TokenController.cs / OAuthController.cs,OpenIddict OAuth/OIDC 標準)
|
||||
- `POST /auth/login`, `/auth/refresh`, `/auth/logout`, `/auth/register`, `/auth/password/{forgot,reset}`, `/auth/email/verify`(AuthController.cs)
|
||||
- `GET/PUT /user/profile`(UserController.cs)
|
||||
- Admin endpoints(tenants / oauth-clients / newsletter-lists)
|
||||
- `POST /integrations/send-engine/webhook-clients/upsert`(SendEngineIntegrationController.cs)
|
||||
- `POST/GET /subscriptions`(SubscriptionsController.cs)
|
||||
- `POST/GET /newsletter`(NewsletterController.cs)
|
||||
|
||||
**MC source 完全沒有 `/file-access/download-tokens` endpoint,也沒有任何 file / download / token issuance 相關的 controller**。ADR-014 §2 line 60-68 描述的「MC POST /file-access/download-tokens」流程從未存在。
|
||||
|
||||
#### 致命發現 2:MC 沒有「validate delegated download token」endpoint
|
||||
|
||||
FAA 端 `/Users/jimchen/file_access_agent/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs` 線 27-72:
|
||||
|
||||
```csharp
|
||||
public async Task<DelegatedDownloadTokenValidationResult> ValidateAsync(
|
||||
DelegatedDownloadTokenValidationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var accessToken = await GetServiceAccessTokenAsync(cancellationToken);
|
||||
// ...
|
||||
using var response = await http.PostAsJsonAsync(
|
||||
_options.DownloadTokenValidationPath, // ← 對 MC 打的 validation endpoint
|
||||
payload,
|
||||
cancellationToken);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
FAA assume MC 有 `_options.DownloadTokenValidationPath`(一個 token introspection endpoint,例如 `POST /file-access/validate-file-download-token`),但對 MC source 全 grep `download-token` / `file-access` / `validate` 都找不到對應 controller。
|
||||
|
||||
→ 即使 visionA 哪天真的拿到一個 delegated download token、帶去打 FAA `GET /files/{key}`,FAA 也會在 `IDelegatedDownloadTokenValidator.ValidateAsync()` 階段因為 MC 不回 200 而把 token 視為 invalid,回 403。
|
||||
|
||||
#### 致命發現 3:ADR-014 從一開始就 assume 而沒驗 source
|
||||
|
||||
對 ADR-014 line 17-18 寫的:
|
||||
|
||||
> **Innovedus Member Center (MC)** 是 OAuth/OIDC IdP,**同時負責簽 service-to-service token 與 delegated download token**
|
||||
|
||||
line 75:
|
||||
|
||||
> server-to-server 跟 MC 換 delegated token(scope `files:download.delegate`)
|
||||
|
||||
→ 2026-05-02 寫 ADR-014 時沒實際讀 MC source、直接 assume「MC 有 delegated token endpoint」。整條 delegated token 鏈路是 fictional。
|
||||
|
||||
ADR-015 v2.0 §2 line 109-153(2026-05-16 上午寫)也沿用這個 assumption(雖然 v2.0 描述的設計「正確的」是 ADR-014 §2 原意圖,但 ADR-014 §2 原意圖本身就建立在錯誤的 MC 能力假設上)。
|
||||
|
||||
#### 致命發現 4:FAA download endpoint 對 service token 也不接受
|
||||
|
||||
`/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` line 184-254:`GET /files/{**objectKey}` **沒掛 `.RequireAuthorization()`**,改用 `IDelegatedDownloadTokenValidator.ValidateAsync(...)` 驗 delegated download token。
|
||||
|
||||
→ FAA download endpoint 強制只接 delegated token、不接 service token。
|
||||
→ 即使 visionA 從 MC 拿到 valid service token(這條鏈是真的 work 的,已有實證——converter promote 走 client_credentials + `files:upload.write` scope 正常 work),帶去打 FAA `GET /files/{key}` 也會 401 / 403。
|
||||
→ visionA 端**唯一**能 download FAA 上的 NEF 的方法是先取得 delegated token;但 MC 沒有 issue delegated token 的 endpoint(致命發現 1)。
|
||||
|
||||
#### 為什麼這個缺口從 2026-05-02 一直沒被發現
|
||||
|
||||
時間軸:
|
||||
|
||||
| 日期 | 事件 | 影響 |
|
||||
|------|------|------|
|
||||
| 2026-05-02 | ADR-014 v1.0 寫定 | assume MC 有 delegated token endpoint |
|
||||
| 2026-05-09 ~ 11 | Phase 0.8 stage 部署遇 4 個 OAuth client_credentials blocker(converter `:read/write` scope 沒 onboard / converter image 舊版 / converter MC env / FAA OAuth 整合不明)| 發現的是 converter 線、不是 FAA delegated token 線 |
|
||||
| 2026-05-11 | ADR-015 v1.0:兩條都改 API key | FAA 線改 API key、跳過了 delegated token 鏈、致命缺口被掩蓋 |
|
||||
| 2026-05-15 | ADR-015 v1.1:補 reference middleware | 仍是 API key 路徑 |
|
||||
| 2026-05-16(上午) | ADR-015 v2.0:撤回 FAA API key、回到 ADR-014 §2 delegated token 設計 | **重新踩到 2026-05-02 的缺口**(但寫 v2.0 時還沒驗 MC source、再次 assume)|
|
||||
| 2026-05-16(下午 / 本 ADR) | 對 MC source 全 grep 確認 → 缺口從未修復 | 撤回 v2.0 §2、改走 converter 中轉 |
|
||||
|
||||
**Phase 0.8 從來沒有真的把 download 跑到 e2e**:T4 frontend `<a href>` 觸發 download 的測試都是對 stub backend,從未真的觸發 visionA → MC issue delegated token → FAA validate 整條鏈,所以 4 處致命缺口疊起來成為一個從未被執行的 dead path。
|
||||
|
||||
### 使用者拍板的硬約束
|
||||
|
||||
2026-05-16 在發現上述缺口後,使用者明確拍板:
|
||||
|
||||
| 約束 | 理由 |
|
||||
|------|------|
|
||||
| **不動 MC** | MC 是公司共用 IdP;新加 endpoint 需 MC team 設計 + onboard scope + redeploy + 配 tenant,跨人協調週期長;過去 5/9 撞 scope 沒註冊已驗證此風險 |
|
||||
| **不動 FAA** | FAA 是 warrenchen 維護的公司共用 repo;既有 dual-auth 設計(JWT for write / delegated for download)改一行都要走跨人協調 |
|
||||
| 但要解決 download | PRD Phase 0.8 範圍內 user 要能下載 NEF(半自動「下載」按鈕 + Wireframe success card 都有) |
|
||||
|
||||
→ 解決方案必須在 jimchen 自己維護的 repo(visionA + converter)內完成、不觸碰 MC / FAA。
|
||||
|
||||
### 既有可用基礎建設
|
||||
|
||||
| 元件 | 狀態 | 可用性 |
|
||||
|------|------|--------|
|
||||
| converter promote → FAA PUT NEF(OAuth + `files:upload.write`) | Phase 1 已上線 | ✅ work(已在 prod 跑 / `apps/task-scheduler/src/fileAccessAgent/client.js`)|
|
||||
| converter MinIO 存 NEF | converter scheduler 既有 | ✅ promote 後 NEF 同時保留在 converter MinIO + FAA(converter expires_at = 7 天)|
|
||||
| visionA → converter API key(ADR-015 §1)| commit `86b7175` 已實作 | ✅ work(init / poll / promote 都已驗證)|
|
||||
| converter scheduler 是 jimchen 自己 repo | — | ✅ 加新 endpoint 對 coordination cost 低 |
|
||||
| visionA backend stream proxy 結構(T4 已實作 `internal/conversion/faa_client.go` `DownloadStream` + `io.CopyN` size cap + `Content-Disposition` + context cancellation)| commit `e02059e` 已實作 | ✅ 結構保留、只是 stream 來源從 FAA 改 converter |
|
||||
|
||||
---
|
||||
|
||||
## 決策 (Decision)
|
||||
|
||||
### 採 **converter 加 `GET /api/v1/jobs/{id}/result` endpoint + visionA stream 中轉** 解決 download;promote 路徑不變。
|
||||
|
||||
### 1. converter 新增 endpoint:`GET /api/v1/jobs/{id}/result`
|
||||
|
||||
#### 1.1 完整 API spec
|
||||
|
||||
```
|
||||
GET /api/v1/jobs/{id}/result HTTP/1.1
|
||||
Host: 192.168.0.130:9501
|
||||
Authorization: Bearer <CONVERTER_API_KEY>
|
||||
Accept: application/octet-stream
|
||||
```
|
||||
|
||||
| 元素 | 規格 |
|
||||
|------|------|
|
||||
| Method | `GET` |
|
||||
| Path | `/api/v1/jobs/:id/result` |
|
||||
| Path param `id` | converter job id(UUID v4) |
|
||||
| Auth | `Authorization: Bearer <CONVERTER_API_KEY>`(同 ADR-015 §1 既有 middleware;converter middleware `subtle.ConstantTimeCompare` 比對 env) |
|
||||
| Query params | 無 |
|
||||
| Request body | 無 |
|
||||
|
||||
#### 1.2 Response — 成功(200)
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/octet-stream
|
||||
Content-Length: <NEF binary 真實大小>
|
||||
Content-Disposition: attachment; filename="<source_filename_stem>_<target_chip_lower>.nef"
|
||||
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
|
||||
|
||||
<NEF binary stream>
|
||||
```
|
||||
|
||||
| Header | 來源 / 規格 |
|
||||
|--------|-----------|
|
||||
| `Content-Type` | 固定 `application/octet-stream` |
|
||||
| `Content-Length` | converter MinIO 物件實際大小(必填,給 visionA backend 設 Content-Length 透傳到 browser)|
|
||||
| `Content-Disposition` | converter 自行構造 filename:`<source_filename_stem>_<target_chip_lower>.nef`(例 `yolov5s_kl720.nef`,對齊 wireframe success card 顯示範例)。**visionA backend 收到後可選擇透傳或覆寫**(visionA 端有自己的 `defaultDownloadFilename(cj)` helper)|
|
||||
| `Cache-Control` | 固定 `no-store, no-cache, must-revalidate, max-age=0` |
|
||||
| Body | NEF binary stream(converter 從 MinIO get object 後直接 stream 出來) |
|
||||
|
||||
#### 1.3 Response — 錯誤
|
||||
|
||||
| HTTP | code | 條件 | converter 回 body(JSON) |
|
||||
|------|------|------|--------------------------|
|
||||
| 401 | `unauthorized` | API key 不對 / 缺 / 格式錯 | `{"error":"unauthorized"}`(同 ADR-015 §3.5.1 既有 middleware)|
|
||||
| 404 | `job_not_found` | job_id 不存在 / 已被 GC(converter 7 天 expires_at)| `{"error":"job_not_found"}` |
|
||||
| 409 | `job_not_completed` | job 尚未 completed(status ≠ completed)| `{"error":"job_not_completed","status":"<current>"}` |
|
||||
| 410 | `result_expired` | job completed 但 MinIO 內 NEF 已被 GC(超 expires_at) | `{"error":"result_expired"}` |
|
||||
| 500 | `internal_error` | converter / MinIO 故障 | `{"error":"internal_error"}` |
|
||||
| 502 | `storage_unavailable` | MinIO 5xx / 不可達 | `{"error":"storage_unavailable"}` |
|
||||
| 503 | `service_busy` | converter 過載 | `{"error":"service_busy"}` |
|
||||
|
||||
#### 1.4 Size cap
|
||||
|
||||
converter 端不設 size cap(converter MinIO 容量為準)。visionA backend 端用 `io.CopyN(w, stream, 1 GiB)` 設 1 GiB 上限保護(同 conversion.md §4.1 既有設計)。
|
||||
|
||||
#### 1.5 與既有 endpoint 的關係
|
||||
|
||||
converter 既有 `POST /api/v1/jobs/:id/download-tokens` 是 Phase 2 預留 501(見 `apps/task-scheduler/src/routes/v1/jobs.js` line 1059-1066)。本 ADR 新增的 `GET /api/v1/jobs/:id/result` 是 Phase 0.8b 的「直接 stream 中轉」實作,**不取代** Phase 2 的 download-tokens endpoint(後者是「給 browser 直連 converter」設計,Phase 2 才評估)。
|
||||
|
||||
#### 1.6 converter 端實作要點(給 jimchen 跨 repo 任務)
|
||||
|
||||
- Route 加在 `apps/task-scheduler/src/routes/v1/jobs.js`(或新建 `result.js` 並在 `v1/index.js` `router.use(...)`)
|
||||
- 套用既有 `requireReadAuth` middleware(與 `GET /api/v1/jobs/:id` 同一 scope;API key 比對由 middleware 統一)
|
||||
- handler 流程:
|
||||
1. 從 jobService 查 job(status 必須是 `completed`、未過 expires_at)
|
||||
2. 從 MinIO get object(`target_object_key` 或 promote 後同步保留在 MinIO 的物件 key)
|
||||
3. set Content-Type / Content-Length / Content-Disposition / Cache-Control headers
|
||||
4. `pipeline(minioStream, res)` 直接 stream(不暫存記憶體)
|
||||
- Test 加 `apps/task-scheduler/src/routes/v1/__tests__/result.integration.test.js`(mock MinIO + jobService)
|
||||
|
||||
### 2. visionA download flow(撤回 FAA / MC 鏈)
|
||||
|
||||
#### 2.1 流程
|
||||
|
||||
```
|
||||
Browser ──GET /api/conversion/{job_id}/download (Cookie: visiona_session)──► visionA backend
|
||||
↓
|
||||
AuthMiddleware (OIDC cookie)
|
||||
↓
|
||||
ownership 檢查 (user_id ↔ job_id)
|
||||
↓
|
||||
ensurePromoted (對 converter 冪等 promote)
|
||||
↓
|
||||
converter_client.GetResult(ctx, job_id)
|
||||
GET {CONVERTER_BASE_URL}/api/v1/jobs/{id}/result
|
||||
Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||||
↓
|
||||
(converter middleware: ConstantTimeCompare)
|
||||
↓
|
||||
200 NEF stream + Content-Length + Content-Disposition
|
||||
↓
|
||||
visionA backend handler:
|
||||
- Content-Type: application/octet-stream
|
||||
- Content-Length: <size>
|
||||
- Content-Disposition: attachment; filename="..."
|
||||
(visionA 用自己的 defaultDownloadFilename(cj) 覆寫、保留與 wireframe 對齊)
|
||||
- Cache-Control: no-store, no-cache, must-revalidate
|
||||
- io.CopyN(c.Writer, stream, 1 GiB)
|
||||
↓
|
||||
Browser 收到 200 + attachment + 自動觸發下載
|
||||
```
|
||||
|
||||
#### 2.2 visionA backend 既有 stream proxy 結構保留
|
||||
|
||||
T4 已實作的 `internal/conversion/faa_client.go` 的 stream proxy 結構(io.CopyN + size cap + Content-Disposition + context cancellation)**全部沿用**,只是 stream 來源從 FAA 改 converter。
|
||||
|
||||
對應的程式碼變更(給 backend agent 下次任務):
|
||||
|
||||
- `internal/conversion/faa_client.go` → **改名** `internal/conversion/converter_result_client.go`(或合併進 `converter_client.go`);移除 `DownloadWithDelegated` 簽名;新增 `GetResult(ctx, jobID) (io.ReadCloser, *DownloadMetadata, error)`
|
||||
- `internal/conversion/flow.go` DownloadStream 內部:移除「IssueDelegatedDownload」步驟、直接呼叫 `converter.GetResult`
|
||||
- `internal/conversion/mc_token_client.go` **整檔刪除**(ADR-015 v2.0 §6 部分復活 → 本 ADR 再次砍除;Phase 0.8b visionA 端不再有任何 visionA → MC server-to-server 路徑)
|
||||
- `internal/conversion/conversion.go` Service interface:`DownloadStream` 簽名不變(仍回 `(io.ReadCloser, *DownloadMetadata, error)`)
|
||||
- handler `internal/api/conversion.go`:`conversionDownloadHandler` 完全不動
|
||||
|
||||
### 3. Promote 路徑完全不變
|
||||
|
||||
promote 仍走 visionA → converter `POST /api/v1/jobs/{id}/promote`:
|
||||
|
||||
```
|
||||
Browser ──POST /api/conversion/{job_id}/promote-to-models──► visionA backend
|
||||
↓
|
||||
converter POST /api/v1/jobs/{id}/promote
|
||||
Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||||
↓
|
||||
converter 內部:用自己的 OAuth client + scope files:upload.write
|
||||
PUT FAA /files/{target_object_key}
|
||||
↓
|
||||
promote 成功;NEF 同時在 FAA 與 converter MinIO
|
||||
↓
|
||||
回 {target_object_key} 給 visionA
|
||||
↓
|
||||
visionA backend 用既有 /api/models/init + /api/models/finalize 流程把 NEF 寫進 visionA model store
|
||||
↓
|
||||
(但注意:visionA 自己不 pull FAA;走的是 converter.GetResult 同一條路徑)
|
||||
```
|
||||
|
||||
「加到模型庫」流程中 visionA 需要拿到 NEF binary 寫進 model store——本 ADR 後此路徑也走 `converter.GetResult`(與 download 共用同一條),visionA 完全不直接打 FAA。
|
||||
|
||||
**converter → FAA 這條鏈仍是 OAuth client_credentials + `files:upload.write` scope**(converter 自己管,與 visionA 無關;本 ADR 不影響)。
|
||||
|
||||
### 4. 認證統一:visionA 端只需 `VISIONA_CONVERTER_API_KEY`
|
||||
|
||||
| 認證 secret | 用途 | Phase 0.8b v0.5(ADR-015 v2.0)| **Phase 0.8b v0.6(本 ADR)** |
|
||||
|------------|------|------------------------------|-----|
|
||||
| `VISIONA_CONVERTER_API_KEY` | visionA → converter(init / poll / promote / **download**)| ✅ 使用中 | ✅ 使用中(**新增 download 走同一把**)|
|
||||
| `VISIONA_OIDC_SERVICE_CLIENT_ID` | visionA → MC 換 service token(給 FAA 線用)| ✅ 重新啟用 | ❌ **再次廢棄**(撤回 v2.0 復活) |
|
||||
| `VISIONA_OIDC_SERVICE_CLIENT_SECRET` | 同上 | ✅ 重新啟用 | ❌ **再次廢棄** |
|
||||
| `VISIONA_OIDC_TENANT_ID` | FAA tenant_id claim 驗證 | ✅ 重新啟用 | ❌ **再次廢棄**(visionA 端不再需要)|
|
||||
| `VISIONA_FAA_BASE_URL` | visionA → FAA 的 base URL | ✅ 使用中 | ❌ **再次廢棄**(visionA 端不再直接打 FAA)|
|
||||
| `VISIONA_FAA_API_KEY` | (v1.0 加的)| ❌ 撤回 | ❌ 維持撤回(v2.0 已撤回) |
|
||||
|
||||
→ visionA 端 server-to-server 認證 secret 只剩 `VISIONA_CONVERTER_API_KEY` 一把(user login 的 OIDC public PKCE 仍維持 ADR-013 設計、不變)。
|
||||
|
||||
### 5. visionA backend mc_token_client 整檔刪除(撤回 v2.0 部分復活)
|
||||
|
||||
ADR-015 v2.0 §6 規劃「mc_token_client.go 部分復活」(保留 ServiceToken cache + IssueDelegatedDownload 邏輯)—— 本 ADR 撤回此規劃。
|
||||
|
||||
- mc_token_client.go 整檔刪除(commit `86b7175` 已經砍掉、不需重建)
|
||||
- mc_token_client_test.go 整檔刪除
|
||||
- `OIDCConfig.ServiceClientID` / `ServiceClientSecret` 兩欄位從 struct 刪除
|
||||
- `ConversionConfig.TenantID` 從 struct 刪除
|
||||
- main.go 不再 wire `MCTokenClient`
|
||||
|
||||
---
|
||||
|
||||
## 考慮過的替代方案 (Alternatives Considered)
|
||||
|
||||
### 方案 A(採用):converter 新增 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | (1) 不動 MC、不動 FAA、不動 warrenchen、不動公司 MC team;(2) 認證統一 API key(jimchen 自己管 rotate);(3) visionA backend 既有 stream proxy 結構保留、改動範圍小;(4) 失敗模式少(只剩 converter 401 / 4xx / 5xx);(5) 修復 ADR-014 §2 從 2026-05-02 起的設計缺口(delegated token 鏈本來就 broken);(6) converter 與 visionA 都是 jimchen 維護、coordination 成本最低;(7) 與 Phase 1 follow-up「converter 加 download-tokens」路徑相容(Phase 2 升級時這個 GET result endpoint 可保留為「server-side pull」用途)|
|
||||
| 缺點 | (1) converter 變 download bottleneck(visionA 跨 internet 從 converter stream、再中轉給 user,雙倍流量);(2) converter MinIO 的 retention 變成 download 可用性 SLA(converter 7 天 expires_at = 7 天後 user 不能 download);(3) converter API 變 download 的單點故障(converter down → user 不能 download,即使 FAA 上有 NEF);(4) 跨 repo 任務(converter scheduler 加 endpoint)— 但因 jimchen 維護兩端、cost 可控 |
|
||||
| 排除原因 | **未排除 — 採用** |
|
||||
|
||||
### 方案 B:metadata-only 模型庫(NEF 永遠在 converter MinIO、不 promote 到 FAA)
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | (1) 不必動 FAA / MC;(2) promote 流程簡化(converter 自己保留 NEF、不 PUT FAA) |
|
||||
| 缺點 | (1) Phase 1 已上線的 converter → FAA promote 流程要拆掉(converter 端改造範圍大);(2) **retention 議題嚴重**:converter MinIO 7 天 expires_at = NEF 7 天後消失、但 visionA model store 設計是「model 是持久資產」、矛盾;(3) 若要延長 converter MinIO retention 必須改 converter scheduler GC 邏輯、影響其他 converter 用途;(4) visionA 「加到模型庫」流程從「pull NEF 進 visionA 自己 storage」變成「永遠依賴 converter MinIO」,違反 ADR-014 §3「加到模型庫 = 進 visionA storage 給後續 device load 用」原意 |
|
||||
| 排除原因 | **retention 模型不可接受**——「加到模型庫」是 user 預期能「永久持有」的動作 |
|
||||
|
||||
### 方案 C:converter promote 改成「直接 stream NEF 回 visionA」
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | promote 同時完成「NEF 已給 visionA」,少一次 round-trip |
|
||||
| 缺點 | (1) promote 是 POST + idempotent 設計、回 stream 違反 REST semantic;(2) promote 同時要做兩件事(推 FAA + 回 stream),失敗模式爆炸(推 FAA 成功但回 stream 失敗時怎麼辦?);(3) frontend wireframe 已設計「先 promote、後 user 選下載 / 加到模型庫」的兩步流程、要改 UX;(4) 「下載」按鈕本來就是 demand-trigger(user 可能 promote 完很久才下載)、把 stream 綁在 promote 不符合使用場景 |
|
||||
| 排除原因 | **promote 設計大改、UX 倒退** |
|
||||
|
||||
### 方案 D:visionA 自己簽 HMAC token、要求 FAA 加新 auth path
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | visionA 不依賴 MC、token 機制比 API key 細粒度(短 TTL + 綁 object_key + 綁 method) |
|
||||
| 缺點 | (1) **動 FAA**:warrenchen 要在 FAA dual-auth(JWT + IDelegatedDownloadTokenValidator)外再加第三條「visionA HMAC」路徑、改公司共用 repo;(2) HMAC secret 需 visionA + FAA 同步管理(與 API key 一樣 long-lived,但多一層複雜度);(3) ADR-015 v2.0 §7 「選項 B」已記為 Phase 1+ follow-up(量大需回 302 redirect 時才考慮)、Phase 0.8b 範圍內動 FAA cost 過高 |
|
||||
| 排除原因 | **使用者硬約束「不動 FAA」**;保留為 Phase 1+ 選項(量大時回 302) |
|
||||
|
||||
### 方案 E:協調 MC team 加 「issue + validate delegated download token」endpoint
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 優點 | 修復 ADR-014 §2 原設計(讓那條鏈真的可走通)、長期看是 OAuth 框架的正確補完 |
|
||||
| 缺點 | (1) **動 MC**:MC team 設計 → onboard scope → 註冊 token endpoint → 配 tenant → redeploy;2026-05-09 撞 MC scope 沒註冊已驗證跨人協調週期長;(2) 即使 MC 補了 endpoint、FAA 端 `MemberCenterDelegatedDownloadTokenValidator._options.DownloadTokenValidationPath` 還要對齊配置;(3) 為了 Phase 0.8b 一個 download path 動兩個公司共用 repo 不划算;(4) Phase 1+ 後若決定走 OAuth 框架可重新評估 |
|
||||
| 排除原因 | **使用者硬約束「不動 MC、不動 FAA」**;長期可重啟此方案(Phase 1+) |
|
||||
|
||||
### 方案 F:放棄 download 功能(只保留「加到模型庫」)
|
||||
|
||||
| 評估 | 內容 |
|
||||
|------|------|
|
||||
| 排除原因 | **PRD Phase 0.8 明確含 download**(wireframe success card「下載」按鈕 + flow-conversion.md 半自動分流);不能放棄 |
|
||||
|
||||
---
|
||||
|
||||
## 後果 (Consequences)
|
||||
|
||||
### 正面影響
|
||||
|
||||
- **修復 ADR-014 §2 從 2026-05-02 起的設計缺口**:delegated download token 鏈從未存在,本 ADR 撤回對其的依賴後 visionA download 不再有 dead path
|
||||
- **不動 MC、不動 FAA、不動公司 MC team / warrenchen**:跨人協調成本歸零,落地速度最快
|
||||
- **認證統一**:visionA 端 server-to-server secret 只剩 `VISIONA_CONVERTER_API_KEY` 一把;MC service client / tenant_id 配置全部撤回,env 表簡化
|
||||
- **失敗模式收斂**:原本「visionA → MC 4xx/5xx + MC → service token + MC → delegated token + FAA → service token validate + FAA → delegated token validate」5 條 fail path 收斂為「converter 401 / 4xx / 5xx」3 條
|
||||
- **可觀測性減負**:visionA 端不需追 MC token cache hit rate、MC 失敗率、delegated token issue 失敗率
|
||||
- **mc_token_client.go 整檔可刪**:commit `86b7175` 已砍掉、本 ADR 確認不需復活(撤回 ADR-015 v2.0 §6 部分復活規劃),visionA backend 程式碼庫進一步簡化
|
||||
- **與 frontend 對 user 完全透明**:response shape / call pattern / 錯誤文字一律不變;只是 visionA backend 內部 stream 來源演進
|
||||
|
||||
### 負面影響(接受的取捨)
|
||||
|
||||
- **converter 變 download bottleneck**:原 ADR-014 §2 設計是「browser 直連 FAA」、流量 1×;ADR-015 v2.0 改 server-side stream proxy 後變 2×(FAA → visionA → browser);本 ADR 後仍是 2× 但路徑改 `converter MinIO → visionA → browser`(流量 cost 與 v2.0 相同、但 hop 改變)
|
||||
- **converter MinIO retention 變 download SLA**:converter 7 天 expires_at = 7 天後 user 不能 download。對應的 frontend UX:
|
||||
- `expires_at` 仍由 visionA 透傳給 frontend(`conversion.md` §2.6.2 既有設計)
|
||||
- frontend 在 success card 顯示倒數(「6 天 21 小時後自動清除」)
|
||||
- 過期後按「下載」會收 410 `result_expired`(visionA backend 把 converter 410 透傳)
|
||||
- **converter 是 download 單點故障**:converter scheduler 掛 → user 不能 download,即使 FAA 上有 NEF(沒辦法繞 converter 直接拉 FAA,因為 visionA 沒有 delegated token 路徑)
|
||||
- **「加到模型庫」流程也依賴 converter**:visionA 端 server-side pull NEF 從「直接 FAA」改成「converter result endpoint」、多一 hop(converter 從 MinIO get object 後 stream 給 visionA)
|
||||
- **與 Phase 1+「browser 直連 converter」路徑稍有不一致**:converter Phase 2 預留的 `POST /api/v1/jobs/:id/download-tokens` endpoint 是給 browser 直連 converter 用、與本 ADR 新增的 `GET /api/v1/jobs/:id/result` 是兩條設計不同的 path。Phase 2 升級時需評估兩條 path 是否共存或合併
|
||||
|
||||
### 風險
|
||||
|
||||
| 風險 | 緩解 |
|
||||
|------|------|
|
||||
| converter MinIO 容量規劃失準(大量 user promote 後 NEF 累積)| converter 既有 GC(7 天 expires_at)即會自動清理;Phase 1+ 可加 retention 監控 |
|
||||
| converter scheduler 在 Phase 0.8b 量小可承受 download bottleneck、但 Phase 1+ 量大時不行 | Phase 1+ 升級到方案 D(visionA HMAC token + FAA 加第三條 auth path + 回 302 redirect);本 ADR 不阻擋此升級路徑 |
|
||||
| converter 加新 endpoint 期間(jimchen 跨 repo)若部署不同步 → visionA download 404 | (1) 加 endpoint 是純增量改動、不影響既有 endpoint;(2) visionA backend 在 converter 端 endpoint 上線前部署也安全(既有 download path 會直接 404、不會破壞其他功能);(3) 部署順序:先 converter ship → 再 visionA ship |
|
||||
| converter MinIO 物件不在(job 過期、或 promote 沒同步寫到 MinIO)| converter 端 handler 處理(410 `result_expired`);visionA backend 透傳給 frontend |
|
||||
| converter 端 result endpoint 實作 bug(stream pipeline 寫錯導致記憶體爆)| 加 integration test(mock MinIO 大檔,驗 stream 不暫存)|
|
||||
|
||||
---
|
||||
|
||||
## 配套產出(給後續 Phase)
|
||||
|
||||
### Phase 0.8b 範圍內(本 ADR 觸發的 follow-up)
|
||||
|
||||
#### A. converter 跨 repo(jimchen)
|
||||
|
||||
- 新增 `apps/task-scheduler/src/routes/v1/result.js`(或加進 `jobs.js`):`GET /api/v1/jobs/:id/result` handler
|
||||
- 套用既有 `requireReadAuth` middleware(API key 比對)
|
||||
- handler 內:job status / expires_at 檢查 → MinIO stream pipeline → 設 headers
|
||||
- 整合測試:`apps/task-scheduler/src/routes/v1/__tests__/result.integration.test.js`
|
||||
- 更新 `apps/task-scheduler/openapi.yaml`(加 `GET /api/v1/jobs/{id}/result` path)
|
||||
- 更新 `apps/task-scheduler/README.md`(API 清單加新 endpoint)
|
||||
|
||||
#### B. visionA backend(backend agent 下次任務)
|
||||
|
||||
- 刪 `internal/conversion/mc_token_client.go`(commit `86b7175` 已砍、確認狀態)
|
||||
- 刪 `internal/conversion/mc_token_client_test.go`(確認狀態)
|
||||
- 改 `internal/conversion/faa_client.go`(**改名** `converter_result_client.go` 或併入 `converter_client.go`):
|
||||
- 移除 `DownloadWithDelegated(ctx, delegatedToken, objectKey)`
|
||||
- 新增 `GetResult(ctx, jobID) (io.ReadCloser, *DownloadMetadata, error)`,內部打 `GET {ConverterBaseURL}/api/v1/jobs/{jobID}/result` + `Authorization: Bearer <ConverterAPIKey>`
|
||||
- 改 `internal/conversion/flow.go`:
|
||||
- 移除 `tokens *MCTokenClient` 欄位(撤回 v2.0 §6 加回)
|
||||
- `DownloadStream` 內部:移除 `IssueDelegatedDownload` 步驟、改呼叫 `converter.GetResult`
|
||||
- `PromoteToModels` 內部(如需要從 FAA pull NEF):同樣改呼叫 `converter.GetResult`
|
||||
- 改 `internal/config/config.go`:
|
||||
- `ConversionConfig.TenantID` 欄位刪除(撤回 v2.0 復活)
|
||||
- `ConversionConfig.FAABaseURL` 欄位刪除(visionA 不再直接打 FAA)
|
||||
- `OIDCConfig.ServiceClientID` / `ServiceClientSecret` 欄位刪除
|
||||
- `Enabled()` 簡化:只判 `ConverterBaseURL != "" && ConverterAPIKey != ""`
|
||||
- 改 `cmd/api-server/main.go`:移除 wire `MCTokenClient`、移除傳 FAA 相關 config
|
||||
- 改 `.env.stage.example` / `.env.dev.example`:
|
||||
- 移除 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` / `VISIONA_OIDC_TENANT_ID` / `VISIONA_FAA_BASE_URL`
|
||||
- 保留 `VISIONA_CONVERTER_BASE_URL` / `VISIONA_CONVERTER_API_KEY`
|
||||
- 改對應 unit / integration test:移除 mc_token / faa_client_test 中對 service token / delegated token 的測試;補 `converter_result_client_test.go`
|
||||
|
||||
### Phase 0.9 / Phase 1+ follow-up
|
||||
|
||||
- [ ] converter API key rotate runbook(範圍縮限至 converter,沿用 ADR-015 v2.0 規劃)
|
||||
- [ ] 觀察 converter result endpoint 在 Phase 1 量大時的效能 / 頻寬 cost
|
||||
- [ ] 若量大需回 302 redirect 模式 → 重新評估方案 D(visionA HMAC token + FAA 加第三條 auth path)
|
||||
- [ ] 若 Phase 2+ 要回 OAuth 框架 → 重新評估方案 E(協調 MC / FAA team 補完 delegated token 鏈)
|
||||
|
||||
---
|
||||
|
||||
## 合規性
|
||||
|
||||
- [x] 與使用者確認:本 ADR 採方案 A(converter 加 GET result endpoint)+ 撤回 visionA → FAA / MC 所有 server-to-server 路徑(2026-05-16)
|
||||
- [x] 與 jimchen 確認(同時為 visionA + converter 維護者):converter scheduler 加新 endpoint 由 jimchen 處理
|
||||
- [x] 對 MC source 完整 grep 驗證:MC 沒有 issue / validate delegated download token 的 endpoint(致命發現 1 + 2 + 4)
|
||||
- [x] 對 FAA source 驗證:`MemberCenterDelegatedDownloadTokenValidator.cs` 確實 assume MC 有 validation endpoint、且 FAA download endpoint 強制只接 delegated token(致命發現 4)
|
||||
- [x] 對 ADR-014 / ADR-015 v2.0 對齊:本 ADR 部分 supersede 兩者(ADR-014 §2 整段 + ADR-015 v2.0 §2 整段)
|
||||
- [x] 對 ADR-013 對齊:user login 的 OIDC public PKCE client 不受影響
|
||||
- [x] 與 converter Phase 1 對齊:promote 流程不變、converter → FAA 路徑(OAuth + `files:upload.write`)不變
|
||||
- [ ] ~~與 MC team 協調補 delegated token endpoint~~(撤回,本 ADR 不動 MC)
|
||||
- [ ] ~~與 warrenchen 協調 FAA dual-auth 改造~~(撤回,本 ADR 不動 FAA)
|
||||
- [ ] DevOps:確認 converter 新 endpoint 上線時序(先 converter ship → 再 visionA ship);rotate runbook follow-up
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- 部分 supersedes:
|
||||
- [`adr-014-conversion-integration.md`](./adr-014-conversion-integration.md)(§2 download 整段 + §3 加到模型庫的 FAA pull 部分 + §5 FAA service token cache 部分 + §7 retry 矩陣 FAA/MC row)
|
||||
- [`adr-015-server-to-server-api-key.md`](./adr-015-server-to-server-api-key.md) v2.0(§2 visionA → FAA 整段)
|
||||
- 不影響:
|
||||
- [`adr-013-public-client.md`](./adr-013-public-client.md)(user login 部分)
|
||||
- [`adr-014-conversion-integration.md`](./adr-014-conversion-integration.md) §1(upload streaming proxy)、§4(模組劃分)、§6(user_id trust boundary)
|
||||
- [`adr-015-server-to-server-api-key.md`](./adr-015-server-to-server-api-key.md) v2.0 §1(visionA → converter API key)
|
||||
- 詳細實作(本 ADR 同步更新):
|
||||
- `conversion.md` v0.6(§1 整體 flow、§2 模組設計、§3 服務間認證、§4.1 download handler、§6 錯誤碼、§9 retry 矩陣、§10 安全考量、變更影響清單)
|
||||
- `api/api-conversion.md` v0.6(檔頭 Auth metadata、§3 promote 與 §4 download 錯誤碼、錯誤碼總覽、版本記錄)
|
||||
- `oidc-tdd.md` v0.4(Metadata 區範圍說明、§13.1 環境變數表、§13.1.1 stage env 範例、§13.1.3 server-to-server 雙線設計改寫)
|
||||
- Source code 證據:
|
||||
- MC controller 列表:`/Users/jimchen/member_center/src/MemberCenter.Api/Controllers/*.cs`(8 個檔、無 file/download/token issuance endpoint)
|
||||
- FAA delegated token validator:`/Users/jimchen/file_access_agent/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs` line 27-72
|
||||
- FAA download endpoint:`/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` line 184-254(無 `RequireAuthorization()`)
|
||||
- converter promote → FAA PUT 已實作:`/Users/jimchen/kneron_model_converter/apps/task-scheduler/src/fileAccessAgent/client.js`
|
||||
- converter v1 routes:`/Users/jimchen/kneron_model_converter/apps/task-scheduler/src/routes/v1/jobs.js`(既有 endpoint:POST jobs / GET jobs / GET jobs/:id / POST jobs/:id/promote;POST jobs/:id/download-tokens 預留 501;DELETE jobs/:id 預留 501)
|
||||
- 觸發背景:`.autoflow/progress.md` 2026-05-16「致命發現 → 拍板選 A → ADR-016」段落
|
||||
|
||||
---
|
||||
|
||||
## 版本記錄
|
||||
|
||||
| 日期 | 版本 | 變更 |
|
||||
|------|------|------|
|
||||
| 2026-05-16 | 1.0 | 初版 — 撤回 visionA → FAA delegated download token 鏈(從 ADR-014 §2 起即為 broken design),改走 converter 新增 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉。視 ADR-014 §2 / ADR-015 v2.0 §2 為部分 supersede;user login(ADR-013)/ upload streaming(ADR-014 §1)/ converter API key(ADR-015 §1)全部不變。涉及 visionA + converter 兩個 repo 改造;不動 MC、不動 FAA、不動 warrenchen。 |
|
||||
@ -1,9 +1,10 @@
|
||||
# API — Conversion(轉檔功能,Phase 0.8 / Phase 0.8b)
|
||||
# API — Conversion(轉檔功能,Phase 0.8 / Phase 0.8b v0.6)
|
||||
|
||||
> **base URL**:`https://stage-9527.innovedus.com:9527/`(stage) / `http://localhost:3721`(dev)
|
||||
> **Auth(user → visionA)**:OIDC cookie session(`visiona_session`),參見 `oidc-tdd.md` — 與 Phase 0.8 完全一致,未變
|
||||
> **服務間認證(visionA → converter / FAA)**:**Phase 0.8b 已改為 pre-shared API key**(取代 OAuth client_credentials)— 對 frontend 透明,不影響本 API 契約;詳見 `conversion.md` §3、[`adr/adr-015-server-to-server-api-key.md`](../adr/adr-015-server-to-server-api-key.md)
|
||||
> **同層**:`api/api-spec.md`(總覽)、`conversion.md`(內部設計)、`adr/adr-014-conversion-integration.md`(仍有效部分)、`adr/adr-015-server-to-server-api-key.md`(Phase 0.8b 認證機制)
|
||||
> **服務間認證(visionA → converter)**:**Phase 0.8b v1.0 改為 pre-shared API key**(取代 OAuth client_credentials)— 對 frontend 透明;v0.6 新增同一把 key 也用於 download 路徑(converter `GET /api/v1/jobs/{id}/result`)。詳見 `conversion.md` §3.1 + [ADR-015 §1](../adr/adr-015-server-to-server-api-key.md)
|
||||
> **服務間認證(visionA → FAA / MC)**:**Phase 0.8b v0.6 整段撤回**(v1.x 加的 API key 撤回 / v2.0 改回 MC service token + delegated download token 也撤回)— visionA 端不再有任何 visionA → FAA / visionA → MC 路徑。download 改走 converter 中轉(converter 從自己的 MinIO stream NEF 回 visionA)。詳見 [ADR-016](../adr/adr-016-download-via-converter.md) + `conversion.md` §3
|
||||
> **同層**:`api/api-spec.md`(總覽)、`conversion.md`(內部設計)、`adr/adr-014-conversion-integration.md`(**§2 被 ADR-016 supersede**;§1 upload / §3 半自動分流原則 / §4 模組劃分 / §6 user_id trust boundary 仍有效)、`adr/adr-015-server-to-server-api-key.md` v2.1(**§2 被 ADR-016 supersede**;§1 visionA → converter API key 仍有效)、`adr/adr-016-download-via-converter.md`(v0.6 上位)
|
||||
> **角色**:給 visionA-frontend 實作時的 API 契約
|
||||
|
||||
---
|
||||
@ -73,7 +74,7 @@ multipart fields(**注意:不要帶 user_id,backend 會從 cookie 灌**)
|
||||
| 409 | `active_job_exists` | visionA pre-check / converter | 顯示「你已有進行中任務」+ details.job |
|
||||
| 413 | `payload_too_large` | converter | 提示檔案大小限制 |
|
||||
| 502 | `converter_unavailable` | visionA | 提示「轉檔服務暫時無法使用」+ 重試按鈕 |
|
||||
| 502 | `converter_auth_failed` | visionA(Phase 0.8b 新增)| 同上文字 — frontend 看不出差別;SRE 從 log 排查 API key 同步 |
|
||||
| 502 | `converter_auth_failed` | visionA(Phase 0.8b v1.0 新增;v2.0 維持)| 同上文字 — frontend 看不出差別;SRE 從 log 排查 converter API key 同步 |
|
||||
| 503 | `service_busy` | converter | 提示稍後重試 |
|
||||
|
||||
---
|
||||
@ -182,22 +183,30 @@ Content-Type: application/json
|
||||
| HTTP | code | 處理 |
|
||||
|------|------|-----|
|
||||
| 403 | `forbidden` | 不是該 user 的 job |
|
||||
| 404 | `not_found` | job_id 不存在 |
|
||||
| 404 | `not_found` | job_id 不存在(visionA ownership 找不到、或 converter 端 `GET result` 回 404 `result_not_found`) |
|
||||
| 409 | `job_not_completed` | job 還沒 completed,不能 promote |
|
||||
| 502 | `converter_unavailable` | promote 失敗,可重試 |
|
||||
| 410 | `result_expired` | v0.6 新增:job completed 但 converter MinIO 內 NEF 已過 7 天 expires_at 被 GC,無法 promote |
|
||||
| 502 | `converter_unavailable` | promote 失敗 / converter `GET result` 5xx,可重試 |
|
||||
| 502 | `converter_auth_failed` | converter API key 不同步(運維事件)|
|
||||
| 502 | `faa_unavailable` | FAA pull 失敗,可重試 |
|
||||
| 502 | `faa_auth_failed` | FAA API key 不同步(運維事件)|
|
||||
|
||||
**冪等性**:對同一 `job_id` 重複呼叫;若已建過 model record,回 200 + 既有 model 詳情(不重新建)。
|
||||
|
||||
> **v0.6 註**:撤回 v0.5 的 `mc_token_unavailable` / `download_token_failed` / `faa_unavailable` 三個 code(v0.5 規劃要回收給 FAA 線用)。visionA 端 v0.6 不再有 visionA → MC / visionA → FAA 直接呼叫;promote 內部 NEF pull 改走 `converter.GetResult`,失敗模式收斂為 `converter_unavailable` / `converter_auth_failed` / `result_not_found` / `result_expired`。converter → FAA(promote 時 converter 內部 PUT)是 converter 自己的事、與 visionA 無關(converter 端失敗會在 promote response 中體現為 5xx)。
|
||||
|
||||
---
|
||||
|
||||
## 4. `GET /api/conversion/{job_id}/download`
|
||||
|
||||
「下載」 — **Phase 0.8b:visionA-backend server-side stream proxy**(從 FAA pull NEF stream 後中轉回 browser)。Phase 0.8 原本的「302 redirect to FAA delegated URL」設計因服務間認證改 API key 而退回 proxy 模式,詳見 [ADR-015](../adr/adr-015-server-to-server-api-key.md) §7、`conversion.md` §4.1。
|
||||
「下載」 — **Phase 0.8b v0.6:visionA-backend server-side stream proxy from converter `GET /api/v1/jobs/{id}/result`**。流程演進:
|
||||
|
||||
對 frontend 而言**呼叫方式完全一致**(`<a href>` / `window.location.href`),但 response 從「302 Location」變成「200 + NEF binary stream + Content-Disposition: attachment」。
|
||||
- **Phase 0.8 (ADR-014)**:「302 redirect to FAA delegated URL」(browser 直連 FAA)
|
||||
- **Phase 0.8b v0.4 (ADR-015 v1.x)**:server-side stream proxy(從 FAA pull NEF),token 用 visionA API key
|
||||
- **Phase 0.8b v0.5 (ADR-015 v2.0)**:保留 server-side stream proxy;token 來源改回 MC delegated download token(**對 MC source 驗證後確認設計 fictional、未實際 e2e 跑通**)
|
||||
- **Phase 0.8b v0.6 (ADR-016)**:保留 server-side stream proxy;**stream 來源從 FAA 改 converter `GET /api/v1/jobs/{id}/result`**;visionA 端不再經 MC / FAA
|
||||
|
||||
詳見 [ADR-016](../adr/adr-016-download-via-converter.md)、`conversion.md` §3 / §4.1。
|
||||
|
||||
對 frontend 而言**呼叫方式完全一致**(`<a href>` / `window.location.href`),response 仍是「200 + NEF binary stream + Content-Disposition: attachment」(與 v0.4 / v0.5 一致;只是 visionA backend 內部抓 stream 的下游 endpoint 改變,frontend 無感)。
|
||||
|
||||
### Request
|
||||
|
||||
@ -246,12 +255,11 @@ Frontend **不需處理 token、不需處理 redirect**;`Content-Disposition:
|
||||
|------|------|-----|
|
||||
| 401 | `unauthorized` | 沒登入;redirect /login(前端攔截)|
|
||||
| 403 | `forbidden` | 不是該 user 的 job |
|
||||
| 404 | `not_found` | job_id 不存在 / 已過期 |
|
||||
| 404 | `not_found` | job_id 不存在 / 已過期(visionA ownership 找不到,或 converter `GET result` 回 404 `result_not_found`)|
|
||||
| 409 | `job_not_completed` | job 還沒 completed,不能下載 |
|
||||
| 502 | `converter_unavailable` | promote 失敗(首次下載且尚未 promote 過時可能發生)|
|
||||
| 410 | `result_expired` | v0.6 新增:job completed 但 converter MinIO 內 NEF 已過 7 天 expires_at 被 GC;frontend 顯示「轉檔結果已過期,請重新轉檔」並提供重新轉檔 CTA |
|
||||
| 502 | `converter_unavailable` | promote 失敗(首次下載且尚未 promote 過時可能發生)/ converter `GET result` 5xx |
|
||||
| 502 | `converter_auth_failed` | converter API key 不同步(運維事件,frontend 不需區分)|
|
||||
| 502 | `faa_unavailable` | FAA pull 失敗 |
|
||||
| 502 | `faa_auth_failed` | FAA API key 不同步(運維事件,frontend 不需區分)|
|
||||
|
||||
**錯誤回應格式**:依 `Accept` header:
|
||||
- `Accept: application/json` → `{success:false, error:{code, message}}`
|
||||
@ -259,8 +267,11 @@ Frontend **不需處理 token、不需處理 redirect**;`Content-Disposition:
|
||||
|
||||
**注意**:
|
||||
- 每次「下載」按鈕都直接打 `/download` endpoint,不要前端 cache 任何中間狀態
|
||||
- Phase 0.8b 退回 server-side proxy 後,visionA backend 變 streaming bottleneck — Phase 1 量大評估升級(ADR-015 §7 選項 B)
|
||||
- 不會與 `promote-to-models` 衝突;兩者內部都會 ensurePromoted(冪等),兩條路徑都拿同一個 target_object_key
|
||||
- Phase 0.8b(v0.4 / v0.5 / v0.6)server-side proxy 模式下,visionA backend 變 streaming bottleneck — Phase 1+ 量大評估升級(converter Phase 2 download-tokens 讓 browser 直連 converter;或 FAA HMAC token)
|
||||
- 不會與 `promote-to-models` 衝突;兩者內部都會 ensurePromoted(冪等),兩條路徑都從 converter `GET result` 拉同一份 NEF stream
|
||||
- v0.6 後 `promote-to-models` 也走 `converter.GetResult`(與 download 共用同一條 path;不再有 delegated token / FAA pull 任何概念)
|
||||
|
||||
> **v0.6 註**:撤回 v0.5 加的 `mc_token_unavailable` / `download_token_failed` / `faa_unavailable` 三個 code。對 frontend 文字仍維持一致(`轉檔服務暫時無法使用`),用 `converter_unavailable` 統一表達;`result_expired`(410)是新增的明確 case,給 frontend 「過期」精確提示。
|
||||
|
||||
---
|
||||
|
||||
@ -318,7 +329,7 @@ Frontend 在「轉檔」入口的 `/conversion` 頁載入時打這個 endpoint
|
||||
|
||||
對齊 `conversion.md` §6。前端 i18n key 統一 `conversion.error.<short-name>`。
|
||||
|
||||
> **Phase 0.8b 變更**:移除 4 個 MC 相關錯誤碼(`download_token_failed` / `mc_token_unavailable` / `idp_unavailable` / `idp_misconfigured`)— 服務間認證取消 MC 依賴。新增 2 個 `*_auth_failed` 錯誤碼對應 API key 不同步的運維事件(frontend 不需區分,UX 文字仍是「服務暫時無法使用」)。
|
||||
> **Phase 0.8b v0.6 變更**:撤回 v0.5「回收 MC 相關 code」決定。visionA 端不再有任何 visionA → MC / visionA → FAA 直接呼叫,`mc_token_unavailable` / `download_token_failed` / `faa_unavailable` 三個 code 全部移除。新增 `result_not_found`(404)與 `result_expired`(410)對應 converter `GET result` endpoint 的新失敗模式。
|
||||
|
||||
| code | HTTP | i18n key | 預設訊息(zh-TW) |
|
||||
|------|------|----------|------------------|
|
||||
@ -326,25 +337,42 @@ Frontend 在「轉檔」入口的 `/conversion` 頁載入時打這個 endpoint
|
||||
| `unauthorized` | 401 | `common.error.unauthorized` | 請先登入 |
|
||||
| `forbidden` | 403 | `conversion.error.forbidden` | 你無權存取此任務 |
|
||||
| `not_found` | 404 | `conversion.error.not_found` | 任務不存在 |
|
||||
| `result_not_found` | 404 | `conversion.error.not_found` | 任務不存在(v0.6 新增:converter `GET result` 回 404;i18n 與 `not_found` 共用文字、SRE 從 log 區分)|
|
||||
| `active_job_exists` | 409 | `conversion.error.active_job` | 你目前已有進行中的轉檔任務 |
|
||||
| `job_not_completed` | 409 | `conversion.error.not_completed` | 任務尚未完成(`promote-to-models` 與 `download` 共用) |
|
||||
| `result_expired` | 410 | `conversion.error.result_expired` | 轉檔結果已過期,請重新轉檔(v0.6 新增:converter `GET result` 回 410;frontend 顯示重新轉檔 CTA)|
|
||||
| `payload_too_large` | 413 | `conversion.error.too_large` | 檔案超過大小限制 |
|
||||
| `converter_unavailable` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用 |
|
||||
| `converter_auth_failed` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用(Phase 0.8b 新增 — frontend 文字同 converter_unavailable)|
|
||||
| `faa_unavailable` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用 |
|
||||
| `faa_auth_failed` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用(Phase 0.8b 新增)|
|
||||
| `converter_auth_failed` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用(v1.0 新增;v2.0 / v0.6 維持 — frontend 文字同 converter_unavailable)|
|
||||
| `service_busy` | 503 | `conversion.error.busy` | 系統繁忙,請稍後再試 |
|
||||
|
||||
**Phase 0.8b 移除(不會再出現的舊 code)**:
|
||||
**Phase 0.8b v0.6 撤回(v0.5 加的、v0.6 移除)**:
|
||||
|
||||
| 已移除 | 原 HTTP | 原語意 |
|
||||
|------|---------|------|
|
||||
| `idp_misconfigured` | 500 | MC token endpoint 4xx |
|
||||
| `idp_unavailable` | 503 | MC token endpoint 5xx |
|
||||
| `download_token_failed` | 502 | MC delegated token 4xx |
|
||||
| `mc_token_unavailable` | 502 | MC 持續失敗 |
|
||||
| 已撤回 | 原 HTTP | 原語意(v0.5)| v0.6 替代 |
|
||||
|------|---------|-------|---|
|
||||
| `mc_token_unavailable` | 502 | MC `/oauth/token` 4xx / 5xx | 不需要(visionA 端不再打 MC)|
|
||||
| `download_token_failed` | 502 | MC `/file-access/download-tokens` 4xx / 5xx | 不需要(visionA 端不再 issue delegated token)|
|
||||
| `faa_unavailable` | 502 | FAA pull 失敗 | 不需要(visionA 端不再直接打 FAA;converter `GET result` 失敗統一收斂進 `converter_unavailable`)|
|
||||
|
||||
frontend i18n 字典可保留 `conversion.error.idp_down` / `conversion.error.token_failed` 兩個 key 暫不刪除(防舊版 client 拿到舊 error code 時還能翻譯),但新版 backend 已不再回這 4 個 code。
|
||||
**Phase 0.8b v0.4 → v0.5 → v0.6 演進摘要**:
|
||||
|
||||
| code | v0.4 狀態 | v0.5 狀態 | **v0.6 狀態** |
|
||||
|------|---------|---------|---|
|
||||
| `converter_auth_failed` | 新增 | 維持 | **維持**(同一把 key 也用於 GetResult)|
|
||||
| `converter_unavailable` | 維持 | 維持 | **維持**(含 `GET result` 5xx 收斂)|
|
||||
| `result_not_found` | — | — | **新增**(converter `GET result` 404)|
|
||||
| `result_expired` | — | — | **新增**(converter `GET result` 410)|
|
||||
| `faa_auth_failed` | 新增 | 撤回 | 維持撤回 |
|
||||
| `faa_unavailable` | 使用中 | 使用中 | **撤回** |
|
||||
| `mc_token_unavailable` | 移除 | 回收 | **撤回** |
|
||||
| `download_token_failed` | 移除 | 回收 | **撤回** |
|
||||
| `idp_misconfigured` | 移除 | 維持移除 | 維持移除 |
|
||||
| `idp_unavailable` | 移除 | 維持移除 | 維持移除 |
|
||||
|
||||
frontend i18n 字典:
|
||||
- v0.6 後 `conversion.error.faa_down` i18n key 可保留但不再被任何 code 引用(向下相容;舊版 backend 不會發 `faa_*` / `mc_*` 三個 code)
|
||||
- **新增 i18n key**:`conversion.error.result_expired` —「轉檔結果已過期,請重新轉檔」(給 410 用、與其他 502 文字明確區分)
|
||||
- `result_not_found` 與 `not_found` 共用 `conversion.error.not_found` i18n key(user 角度兩者等價:任務不存在)
|
||||
|
||||
---
|
||||
|
||||
@ -356,3 +384,5 @@ frontend i18n 字典可保留 `conversion.error.idp_down` / `conversion.error.to
|
||||
| 2026-04-30 | 0.2 | §4 download endpoint 從 `POST /{job}/download-token`(回 JSON `{download_url, expires_at}`)改為 `GET /{job}/download`(HTTP 302 redirect),仿 FAA TestSite `DownloadFileDirect` pattern;token 不過 frontend JS、不需 FAA CORS;`job_not_completed` HTTP code 從 400 改為 409 + 補 `mc_token_unavailable` |
|
||||
| 2026-04-30 | 0.3 | Phase 0.8 三方交叉審閱回饋整合:Job response shape 補 `expires_at` / `source_filename` / `target_chip`(議題 #7);`/api/conversion/active` 行為文件化 lazy rebuild 機制(議題 #2 重啟恢復);`promote-to-models` request body 對齊 Design 單欄位(議題 #4,`description` 留 Phase 1) |
|
||||
| 2026-05-11 | 0.4 | **Phase 0.8b** 對應 [ADR-015](../adr/adr-015-server-to-server-api-key.md):(1) Header / 文件 metadata 標示服務間認證改 pre-shared API key(對 frontend 透明);(2) §4 download endpoint response 從「302 Location」改為「200 + NEF binary + `Content-Disposition: attachment`」— frontend 呼叫方式(`<a href>` / `window.location.href`)完全一致;(3) 錯誤碼總覽:移除 4 個 MC 相關 code(`idp_misconfigured` / `idp_unavailable` / `download_token_failed` / `mc_token_unavailable`),新增 2 個 `*_auth_failed`(`converter_auth_failed` / `faa_auth_failed`)對應 API key 不同步的運維事件 |
|
||||
| 2026-05-16 | 0.5 | **對應 ADR-015 v2.0 範圍縮限**:撤回 v0.4「visionA → FAA 改 API key」;FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token);visionA → converter API key 路線(v0.4)**維持**。主要變更:(1) Header metadata 區分 converter 線 / FAA 線兩條 server-to-server 認證;(2) §3 promote-to-models 與 §4 download endpoint 錯誤碼回收 `mc_token_unavailable` / `download_token_failed`,撤回 v0.4 加的 `faa_auth_failed`;(3) 錯誤碼總覽表更新對應;(4) §4 download endpoint 描述更新「v0.4 server-side proxy + v0.5 token 來源改回 delegated download token」演進。**Frontend 行為對 user 完全透明**:response shape / call pattern / 錯誤文字一律不變;只是 visionA backend 內部 token 來源演進。 |
|
||||
| 2026-05-16 | 0.6 | **對應 [ADR-016](../adr/adr-016-download-via-converter.md)**:撤回 v0.5「visionA → FAA 改回 MC service token + delegated download token」**全部規劃**。原因:對 MC source 全 grep 驗證後確認 MC 沒有 `POST /file-access/download-tokens` endpoint、也沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint—— v0.5 設計是 fictional。**v0.6 新設計**:visionA download 改走 converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉;visionA 端不再有任何 visionA → FAA / visionA → MC 路徑、server-to-server 認證收斂為單條 visionA → converter(API key)。主要變更:(1) Header metadata 區段改寫「服務間認證(visionA → FAA / MC)」段落,整段撤回;(2) §3 promote-to-models 錯誤表移除 `mc_token_unavailable` / `download_token_failed` / `faa_unavailable`,新增 `result_expired`(410);(3) §4 download endpoint 描述更新 v0.6 演進、錯誤表同樣移除 v0.5 加的 MC / FAA 三個 code、新增 `result_expired`;(4) 錯誤碼總覽表更新 — 新增 `result_not_found`(404) + `result_expired`(410)兩個 code,撤回 `faa_unavailable` / `mc_token_unavailable` / `download_token_failed`;(5) v0.4 → v0.5 → v0.6 演進摘要表加 v0.6 column;(6) i18n 字典補 `conversion.error.result_expired` 新 key。**Frontend 行為對 user 仍完全透明**(response shape / call pattern 不變;只是 visionA backend 內部 stream 來源從 FAA 改為 converter)。**新增 frontend 需處理**:410 `result_expired` 顯示「請重新轉檔」CTA(i18n key `conversion.error.result_expired`)。 |
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# Conversion — 轉檔功能整合(Phase 0.8 / Phase 0.8b)
|
||||
|
||||
> **角色**:visionA-backend 端的「轉檔」實作 spec(內部模組設計 + API + flow)。
|
||||
> **上位文件**:[`adr/adr-015-server-to-server-api-key.md`](./adr/adr-015-server-to-server-api-key.md)(Phase 0.8b 認證機制 — 部分 supersede ADR-014)、[`adr/adr-014-conversion-integration.md`](./adr/adr-014-conversion-integration.md)(仍有效:upload streaming、download 302 設計、模組劃分等)、`TDD.md`、`security.md`
|
||||
> **上位文件**:[`adr/adr-016-download-via-converter.md`](./adr/adr-016-download-via-converter.md)(**v0.6 新增**:visionA download 改走 converter 中轉、撤回所有 visionA → MC / FAA 鏈)、[`adr/adr-015-server-to-server-api-key.md`](./adr/adr-015-server-to-server-api-key.md) **v2.1**(visionA → converter API key 維持;§2 visionA → FAA 整段被 ADR-016 supersede)、[`adr/adr-014-conversion-integration.md`](./adr/adr-014-conversion-integration.md)(**§2 download 整段被 ADR-016 supersede**;§1 upload streaming / §3 半自動分流原則 / §4 模組劃分 / §6 user_id trust boundary 仍有效)、`TDD.md`、`security.md`
|
||||
> **同層文件**:`api/api-conversion.md`(對 frontend 的 API 規格細節)
|
||||
> **作者**:Architect Agent
|
||||
> **狀態**:Phase 0.8b 修訂(v0.4)— OAuth client_credentials 改 pre-shared API key
|
||||
> **最後更新**:2026-05-11
|
||||
> **狀態**:Phase 0.8b v0.6 修訂 — visionA → converter 走 API key(不變);**visionA → FAA / MC 兩條鏈完全撤回**;download 改走 converter `GET /api/v1/jobs/{id}/result` 中轉
|
||||
> **最後更新**:2026-05-16
|
||||
|
||||
---
|
||||
|
||||
@ -26,13 +26,17 @@
|
||||
|
||||
## 1. 整體 flow(端對端)
|
||||
|
||||
> **Phase 0.8b 變更**:服務間認證從「打 MC 換 OAuth service token + JWKS 驗簽 + scope」改為「visionA 帶 `Authorization: Bearer <pre-shared-api-key>` 直接打 converter / FAA」。詳見 §3 與 ADR-015。
|
||||
> **Phase 0.8b v0.6 變更**:visionA 端 server-to-server 鏈路**收斂為單條**(只剩 visionA → converter):
|
||||
> - visionA → converter:`Authorization: Bearer <VISIONA_CONVERTER_API_KEY>`(ADR-015 §1,不變)
|
||||
> - visionA → FAA / MC:**完全撤回**(v0.5 加回的鏈是 broken design;對 MC source 全 grep 驗證後確認 MC 沒有 delegated download token endpoint)
|
||||
> - download:visionA → converter `GET /api/v1/jobs/{id}/result`,converter 從 MinIO stream NEF 回 visionA,再 io.CopyN 中轉給 browser(ADR-016)
|
||||
> - 詳見 §3 與 [ADR-016](./adr/adr-016-download-via-converter.md)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser
|
||||
participant V as visionA-backend
|
||||
participant C as Converter
|
||||
participant C as Converter (incl. MinIO)
|
||||
participant F as FAA
|
||||
|
||||
Note over B,F: Stage 1 — Init job(streaming upload)
|
||||
@ -54,65 +58,85 @@ sequenceDiagram
|
||||
V-->>B: 整形後 status
|
||||
end
|
||||
|
||||
Note over B,F: Stage 3a — User 選「加到模型庫」
|
||||
B->>V: POST /api/conversion/{job_id}/promote-to-models
|
||||
Note over B,F: Stage 3 — Promote(converter 內部 push FAA;與 visionA 無關)
|
||||
B->>V: POST /api/conversion/{job_id}/promote-to-models (or download trigger)
|
||||
V->>C: POST /api/v1/jobs/{id}/promote<br/>Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||||
C->>F: PUT /files/{key} (NEF — converter 內部認證,與 visionA 無關)
|
||||
C-->>V: {target_object_key}
|
||||
V->>F: GET /files/{key}<br/>Authorization: Bearer <VISIONA_FAA_API_KEY>
|
||||
F->>F: middleware: ConstantTimeCompare(key, FAA_API_KEY)
|
||||
F-->>V: NEF stream
|
||||
C->>F: PUT /files/{target_object_key} (NEF — converter 自己的 OAuth + files:upload.write scope;與 visionA 完全無關)
|
||||
C-->>V: {target_object_key}<br/>(NEF 同時保留在 converter MinIO 7d expires_at)
|
||||
|
||||
Note over B,F: Stage 3a — User 選「加到模型庫」
|
||||
V->>C: GET /api/v1/jobs/{id}/result<br/>Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||||
C-->>V: 200 NEF binary stream (from converter MinIO)
|
||||
V->>V: /api/models/init → /api/models/finalize<br/>(Source=converted, SourceJobID=job_id)
|
||||
V-->>B: 201 {model_id}
|
||||
|
||||
Note over B,F: Stage 3b — User 選「下載」(Phase 0.8b: server-side proxy;非 302 redirect)
|
||||
Note over B,F: Stage 3b — User 選「下載」(Phase 0.8b v0.6: server-side stream proxy from converter)
|
||||
B->>V: GET /api/conversion/{job_id}/download
|
||||
V->>V: AuthMiddleware → user_id + ownership 檢查
|
||||
V->>C: POST /api/v1/jobs/{id}/promote (若還沒 promote)<br/>Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||||
V->>C: POST /api/v1/jobs/{id}/promote (若還沒 promote — 冪等)
|
||||
C-->>V: {target_object_key}
|
||||
V->>F: GET /files/{key}<br/>Authorization: Bearer <VISIONA_FAA_API_KEY>
|
||||
F-->>V: NEF stream
|
||||
V-->>B: stream NEF(visionA backend 中轉,token 結構性不過 browser)
|
||||
V->>C: GET /api/v1/jobs/{id}/result<br/>Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||||
C-->>V: 200 NEF binary stream + Content-Length + Content-Disposition
|
||||
V-->>B: stream NEF(visionA backend io.CopyN 中轉、size cap 1 GiB)
|
||||
```
|
||||
|
||||
**critical path 說明**:
|
||||
|
||||
- visionA-backend 在 upload / download / promote 任一階段都先做 OIDC AuthMiddleware(既有)+ ownership 檢查
|
||||
- promote 動作是冪等的(converter 端對同一 job 重複 promote 接受),visionA-backend 內部以 `job_id ↔ promoted_object_key` 記錄避免重複呼叫
|
||||
- 加到模型庫流程:promote → FAA pull → `/api/models/init` 取得 model_id + presigned PUT URL → visionA-backend 自己 PUT 到 storage → `/api/models/finalize`(這條路徑要重用既有 handler 邏輯,不繞過)
|
||||
- **Phase 0.8b 認證鏈簡化**:不再有 visionA ↔ MC 鏈路;不再有「token cache miss / scope mismatch / JWKS 不可達」失敗模式。converter / FAA 端 middleware 各自只比對單一字串。
|
||||
- 加到模型庫流程(v0.6):promote → **converter.GetResult 拉 NEF stream**(不是直接打 FAA)→ `/api/models/init` 取得 model_id + presigned PUT URL → visionA-backend 自己 PUT 到 storage → `/api/models/finalize`(這條路徑要重用既有 handler 邏輯,不繞過)
|
||||
- download flow(v0.6):promote 冪等 → **converter.GetResult 拉 NEF stream** → io.CopyN 中轉給 browser(size cap 1 GiB)
|
||||
- **Phase 0.8b v0.6 認證鏈說明**:
|
||||
- visionA 端只有一條:visionA → converter(API key、constant-time compare)
|
||||
- converter → FAA:converter 自己用 OAuth client_credentials + `files:upload.write` scope(既有實作 `apps/task-scheduler/src/fileAccessAgent/client.js`、Phase 1 已上線;與 visionA 無關)
|
||||
- visionA → MC:**完全不存在**(user login 的 OIDC public PKCE client 是另一條完全獨立的鏈,不在本文件範圍)
|
||||
- visionA → FAA:**完全不存在**
|
||||
|
||||
**Phase 0.8b 與 ADR-014 的差異說明**:
|
||||
**Phase 0.8b v0.6 與 ADR-014 / ADR-015 v1.x / v2.0 的差異說明**:
|
||||
|
||||
| 面向 | ADR-014(OAuth client_credentials) | Phase 0.8b(API key)|
|
||||
|------|--------------------------------|------------------|
|
||||
| visionA → converter 認證 | `Authorization: Bearer <MC-issued service token>` | `Authorization: Bearer <VISIONA_CONVERTER_API_KEY>` |
|
||||
| visionA → FAA 認證 | `Authorization: Bearer <MC-issued service token>` | `Authorization: Bearer <VISIONA_FAA_API_KEY>` |
|
||||
| download 流程 | server-side 302 redirect → browser 直連 FAA(拿 MC delegated token) | server-side proxy(visionA backend 中轉 stream)|
|
||||
| visionA → MC service token 路徑 | 啟動 lazy init MCTokenClient + cache | **完全移除** |
|
||||
| converter / FAA middleware | 驗 JWKS 簽章 + 驗 scope + 驗 tenant | 比對 env 字串(constant-time)|
|
||||
| 面向 | ADR-014(OAuth client_credentials 兩條線)| ADR-015 v1.x(兩條線都 API key)| ADR-015 v2.0(converter API key + FAA OAuth + delegated)| **ADR-016 / v0.6(visionA 端只剩 converter)**|
|
||||
|------|--------------------------------|------------------|---|---|
|
||||
| visionA → converter 認證 | `Authorization: Bearer <MC-issued service token>` | `Authorization: Bearer <VISIONA_CONVERTER_API_KEY>` | 同 v1.x | 同 v1.x / v2.0(不變)|
|
||||
| visionA → FAA(write / metadata / delete)| `Authorization: Bearer <MC service token>` + scope | `Authorization: Bearer <VISIONA_FAA_API_KEY>` | 回到 ADR-014(service token) | **不存在**(visionA 端不再直接打 FAA)|
|
||||
| visionA → FAA(download `GET /files/{key}`)| `Authorization: Bearer <MC delegated download token>` | `Authorization: Bearer <VISIONA_FAA_API_KEY>` | 回到 ADR-014(delegated token) | **不存在**(visionA 端不再直接打 FAA)|
|
||||
| download 從哪取 NEF | FAA `GET /files/{key}` | 同上 | 同上(fictional — delegated token endpoint MC 沒有)| **converter `GET /api/v1/jobs/{id}/result`**(從 converter MinIO stream)|
|
||||
| download 在 browser 端流程 | 302 redirect | server-side proxy | server-side proxy(同 v0.4)| server-side proxy(同 v0.4 / v0.5,不變)|
|
||||
| visionA → MC service token 路徑 | 啟動 lazy init MCTokenClient + cache | 完全移除 | 部分復活 | **完全移除**(撤回 v2.0 復活;mc_token_client.go 整檔砍除)|
|
||||
| converter middleware | 驗 JWKS + scope + tenant | 比對 env 字串(constant-time)| 同 v1.x | 同 v1.x(不變)|
|
||||
| FAA middleware | 驗 JWKS + scope + tenant + delegated token | API key 比對 env 字串 | 回到 ADR-014(dual-auth) | 不適用(visionA 端不再呼叫 FAA;converter → FAA 仍走 ADR-014 OAuth 路徑、但 converter 自己管)|
|
||||
|
||||
> 為什麼 download 不繼續走 302 redirect:API key 模式下沒有 MC 簽 short-TTL delegated token;visionA 自己簽 HMAC token 給 browser 的方案留給 Phase 1+(見 ADR-015 §7 選項 B)。
|
||||
> **為什麼 v0.6 把 download 拉到 converter 中轉**:
|
||||
>
|
||||
> 1. **v2.0 設計的 delegated token 鏈是 fictional**:對 MC source(`/Users/jimchen/member_center/src/MemberCenter.Api/Controllers/*.cs` 8 個 controller)全 grep 確認 MC 沒有 `POST /file-access/download-tokens` endpoint、也沒有 FAA `MemberCenterDelegatedDownloadTokenValidator` assume 的 introspection endpoint。整條鏈從 2026-05-02 ADR-014 寫定起即為 broken、只是因 visionA 從未實際 e2e 跑通 download 而沒被發現
|
||||
> 2. **使用者硬約束「不動 MC、不動 FAA」**:補 MC endpoint 需 MC team 設計 + onboard scope;補 FAA endpoint 需 warrenchen 改公司共用 repo;跨人協調 cost 高(5/9 撞 scope 沒註冊已驗證)
|
||||
> 3. **converter 是 jimchen 自己 repo**:加 `GET /api/v1/jobs/{id}/result` 對 coordination cost 低
|
||||
> 4. **failure mode 收斂**:visionA 端 fail path 從 5 條(visionA → MC 4xx/5xx、MC token cache、MC delegated token issue、FAA service token validate、FAA delegated token validate)收斂為 3 條(converter 401 / 4xx / 5xx)
|
||||
> 5. **既有 stream proxy 結構保留**:v0.4 / v0.5 的 server-side stream proxy + size cap + context cancellation 完全沿用,只是 stream 來源從 FAA 改 converter
|
||||
>
|
||||
> 詳見 [ADR-016 §決策 + 替代方案](./adr/adr-016-download-via-converter.md)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模組設計 — `internal/conversion/`
|
||||
|
||||
> **Phase 0.8b 變更**:移除 `mc_token_client.go` 整個檔案。converter / FAA client 直接從 config 讀預設 API key。
|
||||
> **Phase 0.8b v0.6 變更**:撤回 v0.5「mc_token_client 部分復活、faa_client 改回 service token + delegated token」設計。download 改走 converter `GET /api/v1/jobs/{id}/result`、stream 來源從 FAA 改 converter。
|
||||
|
||||
```
|
||||
internal/conversion/
|
||||
├── conversion.go # Service interface + 對外暴露的 type
|
||||
├── converter_client.go # converter scheduler API client(直接帶 VISIONA_CONVERTER_API_KEY)
|
||||
├── faa_client.go # FAA API client(pull NEF;直接帶 VISIONA_FAA_API_KEY)
|
||||
├── flow.go # 整體 flow 協調
|
||||
├── types.go # request / response struct
|
||||
└── errors.go # error code 定義
|
||||
├── conversion.go # Service interface + 對外暴露的 type
|
||||
├── converter_client.go # converter scheduler API client(init / poll / promote / GetResult — 帶 VISIONA_CONVERTER_API_KEY)
|
||||
├── (faa_client.go 刪除 / 改名) # v0.6:visionA 端不再直接打 FAA;改名為 converter_result_client.go(或併入 converter_client.go),唯一職責是打 converter GET result endpoint
|
||||
├── (mc_token_client.go 刪除) # v0.6:撤回 v0.5「部分復活」決定;visionA 端不再有任何 visionA → MC server-to-server 路徑
|
||||
├── flow.go # 整體 flow 協調(download / PromoteToModels 都走 converter.GetResult)
|
||||
├── types.go # request / response struct
|
||||
└── errors.go # error code 定義
|
||||
```
|
||||
|
||||
**Phase 0.8b 移除**:
|
||||
- ❌ `mc_token_client.go`(~440 行 — 整檔砍)— 不再需要與 MC 換 service token / delegated download token
|
||||
- ❌ `MCTokenClient` 在 flow / converter_client / faa_client 上的所有 reference
|
||||
**Phase 0.8b v0.6 模組變更摘要(相對於 v0.5)**:
|
||||
- ✅ converter_client.go:維持 v0.5(API key 直接 set header);**新增 `GetResult(ctx, jobID)` method** 用於拉 NEF binary stream
|
||||
- ❌ faa_client.go:**整檔刪除 / 改名**(v0.5 加的「`DownloadWithDelegated` + `tokens *MCTokenClient` 欄位」全部移除;唯一還需要的 stream proxy 結構併入 `converter_client.go` 的 `GetResult` 或新建 `converter_result_client.go`)
|
||||
- ❌ mc_token_client.go:**整檔刪除**(撤回 v0.5「部分復活」;commit `86b7175` 已砍掉,**維持砍除狀態**,不需復活;ServiceToken cache + IssueDelegatedDownload 兩個 method 都不再需要)
|
||||
- ↩ flow.go:撤回 v0.5「DownloadStream / PromoteToModels 走 IssueDelegatedDownload + DownloadWithDelegated」;改成「呼叫 `converter.GetResult(ctx, jobID)`」;`tokens *MCTokenClient` 欄位刪除
|
||||
|
||||
### 2.1 `conversion.go` — 對外 interface
|
||||
|
||||
@ -140,13 +164,16 @@ type Service interface {
|
||||
// 回傳新建的 model_id。
|
||||
PromoteToModels(ctx context.Context, userID, jobID string) (modelID string, err error)
|
||||
|
||||
// DownloadStream — 「下載」流程(Phase 0.8b:server-side proxy):
|
||||
// DownloadStream — 「下載」流程(Phase 0.8b v0.6:server-side stream proxy + converter `GET /api/v1/jobs/{id}/result`):
|
||||
// 1. ownership 檢查
|
||||
// 2. promote (若需要)
|
||||
// 3. 從 FAA pull NEF stream
|
||||
// 4. handler 直接 io.Copy stream 給 client
|
||||
// 不再產生 302 redirect URL(API key 模式下無 MC delegated token)。
|
||||
// 詳見 ADR-015 §7 — Phase 1+ 量大時再評估「visionA 自簽 HMAC token + 302」升級路徑。
|
||||
// 2. ensurePromoted(冪等 cache 對 converter;NEF 確認已在 converter MinIO + FAA)
|
||||
// 3. converter.GetResult(ctx, jobID) — 直接打 converter GET result endpoint
|
||||
// Authorization: Bearer <ConverterAPIKey>(同其他 converter API method)
|
||||
// converter response 200 + NEF binary stream + Content-Length + Content-Disposition
|
||||
// 4. handler 直接 io.CopyN stream 給 client(size cap 1 GiB)
|
||||
// 不產生 302 redirect URL(server-side proxy 在 T4 已實作,v0.4 / v0.5 / v0.6 沿用;不退回 302)。
|
||||
// 不再經過 visionA → MC / visionA → FAA 任何路徑(v0.6 整段撤回;詳見 ADR-016)。
|
||||
// Phase 1+ 量大時可評估方案 D(visionA 自簽 HMAC + FAA 加第三條 auth path + 回 302)。
|
||||
DownloadStream(ctx context.Context, userID, jobID string) (stream io.ReadCloser, meta *DownloadMetadata, err error)
|
||||
|
||||
// ActiveJob 查 user 當前是否有 active job(給 frontend pre-check 用)。
|
||||
@ -177,19 +204,20 @@ type Job struct {
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
// Phase 0.8b:DownloadGrant 移除(不再有 MC delegated token 換取流程)。
|
||||
// Download 走 server-side proxy;token 結構性不過 frontend。
|
||||
// Phase 0.8b v0.6:撤回 v0.5 DownloadGrant struct(不再需要 delegated token 持有結構)。
|
||||
// visionA → converter 一條鏈、沒有 token issue 過程,flow.go 直接呼叫 converter.GetResult
|
||||
// 拿 stream + DownloadMetadata 即可。
|
||||
|
||||
// DownloadMetadata — DownloadStream 回傳的中介資料(沿用 §2.3 既定的型別)。
|
||||
// (定義在 faa_client.go,避免重複)
|
||||
// (定義在 converter_result_client.go / converter_client.go,避免重複)
|
||||
```
|
||||
|
||||
### 2.2 `converter_client.go`
|
||||
### 2.2 `converter_client.go`(v2.0:維持 v1.x — API key 不變)
|
||||
|
||||
```go
|
||||
type ConverterClient struct {
|
||||
baseURL string
|
||||
apiKey string // Phase 0.8b:pre-shared API key(VISIONA_CONVERTER_API_KEY)
|
||||
apiKey string // Phase 0.8b v1.x+v2.0:pre-shared API key(VISIONA_CONVERTER_API_KEY)
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
@ -207,53 +235,59 @@ func (c *ConverterClient) PromoteJob(ctx context.Context, jobID string) (targetO
|
||||
func (c *ConverterClient) ListActiveJobsByUser(ctx context.Context, userID string) (*Job, error)
|
||||
```
|
||||
|
||||
每個方法內部(Phase 0.8b 簡化):
|
||||
每個方法內部(v1.x + v2.0 簡化):
|
||||
|
||||
1. `req.Header.Set("Authorization", "Bearer "+c.apiKey)` — 直接帶 pre-shared API key,**不查 cache、不打 MC、不重簽**
|
||||
2. 帶 `X-Request-Id`(從 ctx 取,沿用 visionA-backend 既有 request_id 中介層)
|
||||
3. response 5xx / network error 走 retry(§9);401/403 不 retry(API key 錯不會自己變對)
|
||||
|
||||
### 2.3 `faa_client.go`
|
||||
### 2.3 ~~`faa_client.go`~~(v0.6:整檔刪除 / 改名為 `converter_result_client.go`)
|
||||
|
||||
> **v0.6 撤回 v0.5 設計**:v0.5 規劃的「`tokens *MCTokenClient` 欄位 + `DownloadWithDelegated(ctx, delegatedToken, objectKey)`」整段刪除。visionA 端不再有 `FAAClient` 概念、不再有任何 `internal/conversion/` 內對 FAA 的呼叫。
|
||||
>
|
||||
> 取而代之:原 `faa_client.go` 的 stream proxy 結構(`io.ReadCloser` + `DownloadMetadata`)改名為 `converter_result_client.go`(或併入 `converter_client.go` 作為其 method),唯一職責是打 converter `GET /api/v1/jobs/{id}/result` 拉 NEF binary stream。
|
||||
|
||||
```go
|
||||
type FAAClient struct {
|
||||
baseURL string
|
||||
apiKey string // Phase 0.8b:pre-shared API key(VISIONA_FAA_API_KEY)
|
||||
httpClient *http.Client
|
||||
}
|
||||
// 新檔案(或併入 converter_client.go):
|
||||
|
||||
// Download server-to-server 拉檔(給「加到模型庫」+「下載」兩個流程共用)。
|
||||
// 帶 Authorization: Bearer <VISIONA_FAA_API_KEY>。
|
||||
func (c *FAAClient) Download(ctx context.Context, objectKey string) (io.ReadCloser, *DownloadMetadata, error)
|
||||
// GetResult — 對 converter GET /api/v1/jobs/{id}/result 拉 NEF binary stream。
|
||||
// 帶 Authorization: Bearer <ConverterAPIKey>(同其他 converter API method)。
|
||||
// converter response 200 + Content-Length + Content-Disposition + body stream。
|
||||
// 對應 converter 端 endpoint spec 詳見 ADR-016 §1。
|
||||
func (c *ConverterClient) GetResult(ctx context.Context, jobID string) (io.ReadCloser, *DownloadMetadata, error)
|
||||
|
||||
type DownloadMetadata struct {
|
||||
SizeBytes int64
|
||||
ContentType string
|
||||
Checksum string // optional
|
||||
SizeBytes int64 // 從 converter response Content-Length 解析
|
||||
ContentType string // 固定 application/octet-stream
|
||||
Filename string // 從 converter response Content-Disposition 解析;visionA 端可選擇覆寫(見 §4.1 註)
|
||||
}
|
||||
```
|
||||
|
||||
**Phase 0.8b**:不再需要 `DownloadGrant`(無 MC delegated token);download flow 用同一個 `FAAClient.Download()` 拉 stream 後由 handler 中轉給 client。
|
||||
**v0.6 重要設計約束**:
|
||||
- 「加到模型庫」flow 與「下載」flow **共用同一個 `GetResult`**——兩條 path 都從 converter MinIO 拉 NEF。visionA 端完全不需理解 FAA 的存在。
|
||||
- size cap:visionA backend handler 端用 `io.CopyN(w, stream, 1 GiB)` 保護;converter 端不另外設 cap(converter MinIO 容量為準)。
|
||||
- failure mapping:
|
||||
- converter 401 → `converter_auth_failed`(運維事件,API key 不同步)
|
||||
- converter 404 → `result_not_found`(job_id 不存在 / 已過 7 天 expires_at)
|
||||
- converter 410 → `result_expired`(job completed 但 NEF 已被 converter MinIO GC)
|
||||
- converter 409 → `job_not_completed`(job 尚未 completed,理論上 visionA 端 ensurePromoted 前已確認、不應發生)
|
||||
- converter 5xx / network → `converter_unavailable`
|
||||
|
||||
### 2.4 ~~`mc_token_client.go`~~(Phase 0.8b 移除)
|
||||
### 2.4 ~~`mc_token_client.go`~~(v0.6:整檔刪除、撤回 v0.5 部分復活)
|
||||
|
||||
> **整個檔案在 Phase 0.8b 移除**。詳見 [ADR-015](./adr/adr-015-server-to-server-api-key.md)。
|
||||
> **v0.6 撤回 v0.5「部分復活」決定**。對 MC source 全 grep 驗證後確認 MC **沒有** `POST /file-access/download-tokens` endpoint、也**沒有** FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint——v0.5 規劃的 `IssueDelegatedDownload` method 是 fictional、永遠 issue 不到 delegated token。
|
||||
>
|
||||
> 原本職責:跟 MC 換 service token(client_credentials grant)+ 換 delegated download token。
|
||||
> **v0.6 處理**:mc_token_client.go 在 commit `86b7175` 已被砍除,**維持砍除狀態**、不需復活。對應的 test、config 欄位(`OIDCConfig.ServiceClientID/Secret`、`ConversionConfig.TenantID`)、env(`VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET`、`VISIONA_OIDC_TENANT_ID`、`VISIONA_FAA_BASE_URL`)全部撤回。
|
||||
>
|
||||
> Phase 0.8b 後:
|
||||
> - 服務間認證直接帶 `Authorization: Bearer <pre-shared API key>` — 不需 cache、不需 refresh、不需 retry MC
|
||||
> - download flow 退回 server-side proxy(visionA backend 中轉 stream),不再有 delegated token 概念
|
||||
>
|
||||
> **Tenant 概念取消**:visionA → converter / FAA 不再帶 tenant_id;converter 端的 user_id 仍由 visionA 灌入(§7 trust boundary 不變)。
|
||||
> visionA 端**不再有任何 visionA → MC server-to-server 路徑**。user login 的 OIDC(PKCE / cookie session / JWKS 驗 id_token)是另一條完全獨立的鏈、不在本文件範圍(詳見 `oidc-tdd.md`)。
|
||||
|
||||
### 2.5 `flow.go` — 流程協調
|
||||
|
||||
```go
|
||||
type Flow struct {
|
||||
converter *ConverterClient
|
||||
faa *FAAClient
|
||||
// Phase 0.8b:不再需要 tokens *MCTokenClient
|
||||
converter *ConverterClient // 含 init / poll / promote / GetResult method
|
||||
// (faa *FAAClient — v0.6 刪除)
|
||||
// (tokens *MCTokenClient — v0.6 刪除,撤回 v0.5 復活)
|
||||
models model.Repository // 沿用既有 model store
|
||||
storage storage.Store // 沿用既有 LocalFS / S3
|
||||
ownership ownershipStore // job_id → user_id mapping (in-memory map)
|
||||
@ -261,19 +295,33 @@ type Flow struct {
|
||||
statusCache *jobStatusCache // 1-2s short cache,避免 frontend polling 直接打爆 converter
|
||||
}
|
||||
|
||||
// 主要 method 對應 Service interface。
|
||||
// PromoteToModels 內部:
|
||||
// 1. ownership.Check(userID, jobID)
|
||||
// 2. promotedKey, err := flow.ensurePromoted(ctx, jobID) // 冪等:若已 promote 過用 cache,否則打 converter
|
||||
// 3. reader, meta, err := faa.Download(ctx, promotedKey)
|
||||
// 4. modelID, putURL, _ := callModelsInit(...) // 直接呼叫 internal/api 同 package 既有 helper(不走 HTTP)
|
||||
// 5. PUT 到 storage(或直接 io.Copy 到 storage.Put)
|
||||
// 6. callModelsFinalize(...)
|
||||
// 7. 在 model record 補 Source="converted" + SourceJobID=jobID
|
||||
// 8. 回 modelID
|
||||
// DownloadStream(v0.6 流程):
|
||||
// 1. ownership.Check(userID, jobID)
|
||||
// 2. _, _ := flow.ensurePromoted(ctx, jobID) // 冪等 cache,確保 converter 端 promote 完成(NEF 已在 MinIO + FAA)
|
||||
// 3. stream, meta, _ := flow.converter.GetResult(ctx, jobID)
|
||||
// 內部:GET {ConverterBaseURL}/api/v1/jobs/{jobID}/result
|
||||
// Authorization: Bearer <ConverterAPIKey>
|
||||
// converter response 200 + NEF binary stream + Content-Length + Content-Disposition
|
||||
// 4. meta.Filename = defaultDownloadFilename(cj) // visionA 自行構造(§4.1 註)覆寫 converter 給的 filename
|
||||
// 5. return stream, meta, nil
|
||||
//
|
||||
// PromoteToModels 內部(v0.6 修正):
|
||||
// 1. ownership.Check(userID, jobID)
|
||||
// 2. _, _ := flow.ensurePromoted(ctx, jobID)
|
||||
// 3. reader, meta, _ := flow.converter.GetResult(ctx, jobID)
|
||||
// ← v0.6:與 DownloadStream 共用同一 method、不需 delegated token
|
||||
// 4. modelID, putURL, _ := callModelsInit(...) // 直接呼叫 internal/api 同 package 既有 helper
|
||||
// 5. PUT 到 storage(或直接 io.Copy 到 storage.Put)
|
||||
// 6. callModelsFinalize(...)
|
||||
// 7. 在 model record 補 Source="converted" + SourceJobID=jobID
|
||||
// 8. 回 modelID
|
||||
|
||||
// 主要 method 對應 Service interface(v0.6 流程已在 struct 上方註解寫出;此處保留結構說明)
|
||||
```
|
||||
|
||||
**冪等性**:`flow.ensurePromoted(jobID)` 內部用 `sync.Map` 記 `job_id → target_object_key`;同 job 第二次 promote 直接回 cache,不打 converter。
|
||||
**冪等性**:`flow.ensurePromoted(jobID)` 內部用 `sync.Map` 記 `job_id → target_object_key`;同 job 第二次 promote 直接回 cache,不打 converter。`target_object_key` 在 v0.6 仍由 promote response 拿到(給 log / debug 用)、但 visionA 不再用它直接打 FAA(converter `GetResult` 內部知道哪個 object 屬於哪個 jobID)。
|
||||
|
||||
**為什麼移除 delegated token 邏輯**:v0.5 規劃「IssueDelegatedDownload + DownloadWithDelegated」依賴 MC 有對應 endpoint 才 work;對 MC source 驗證後確認該 endpoint **從未存在**——v0.5 的設計是 fictional、永遠跑不通。v0.6 把整條鏈撤回、改走 converter 中轉(converter 自己用 OAuth 推 FAA、後續 download 從 converter MinIO 拉)。詳見 [ADR-016](./adr/adr-016-download-via-converter.md)。
|
||||
|
||||
### 2.6 `ownership` store(in-memory)
|
||||
|
||||
@ -350,102 +398,213 @@ func (f *Flow) ActiveJob(ctx context.Context, userID string) (*conversion.Job, e
|
||||
|
||||
---
|
||||
|
||||
## 3. 服務間認證(API key)— 取代 OAuth client_credentials
|
||||
## 3. 服務間認證(v0.6:visionA 端只剩單條 visionA → converter)
|
||||
|
||||
> **Phase 0.8b 變更**:本節為新增;對應 [ADR-015](./adr/adr-015-server-to-server-api-key.md)。
|
||||
> **Phase 0.8b v0.6 變更**:本節重寫;對應 [ADR-016](./adr/adr-016-download-via-converter.md)。
|
||||
>
|
||||
> **歷史**:ADR-014 §5 原本設計為「visionA → MC `POST /oauth/token` 換 service token + cache + 帶 JWT 給 converter / FAA」。Phase 0.8 stage e2e 卡關(MC scope 沒註冊、converter image 舊、跨 repo 配合複雜)後,使用者決策改用 pre-shared API key。
|
||||
> **歷史**:
|
||||
> - **v0.4 (2026-05-11)**:本節將兩條線都改為 pre-shared API key(撤回 ADR-014 §5 OAuth 設計)
|
||||
> - **v0.5 (2026-05-16)**:本節再修,只 converter 線維持 API key,FAA 線回到 ADR-014 §2 原設計(service token + delegated download token)
|
||||
> - **v0.6 (2026-05-16)**:對 MC source 驗證後確認 v0.5 設計的 delegated token 鏈是 fictional(MC 沒有對應 endpoint);本節再次整段改寫——visionA → FAA / MC 鏈**完全撤回**,download 改走 converter `GET /api/v1/jobs/{id}/result`,visionA 端只剩 visionA → converter 一條 server-to-server 認證鏈
|
||||
|
||||
### 3.1 取得流程
|
||||
### 3.1 visionA → converter(API key)— v1.x 設計 + v2.0 / v2.1 / v0.6 維持
|
||||
|
||||
對應 [ADR-015 §1](./adr/adr-015-server-to-server-api-key.md)(v1.x / v2.0 / v2.1 都不變)。**v0.6 新增**:同一把 API key 也用於新增的 `GET /api/v1/jobs/{id}/result` endpoint(ADR-016 §1)。
|
||||
|
||||
#### 3.1.1 取得流程
|
||||
|
||||
```
|
||||
visionA-backend 啟動
|
||||
↓
|
||||
讀 cfg.Conversion.ConverterAPIKey(env VISIONA_CONVERTER_API_KEY)
|
||||
讀 cfg.Conversion.FAAAPIKey(env VISIONA_FAA_API_KEY)
|
||||
↓
|
||||
[轉檔請求進來]
|
||||
↓
|
||||
converter_client / faa_client 發 request 時:
|
||||
converter_client 發 request 時:
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
↓
|
||||
converter / FAA middleware:
|
||||
converter middleware:
|
||||
- parse Authorization header → 取 token
|
||||
- subtle.ConstantTimeCompare(token, envKey)
|
||||
- match → 放行;mismatch → 401 + log(不附原因)
|
||||
```
|
||||
|
||||
**沒有 token cache、沒有 refresh、沒有 retry MC、沒有 scope 驗證**。整條鏈路是「visionA → 下游」一步。
|
||||
**沒有 token cache、沒有 refresh、沒有 retry MC、沒有 scope 驗證**。整條鏈路是「visionA → converter」一步。
|
||||
|
||||
### 3.2 Config 對齊
|
||||
|
||||
`visionA-backend/internal/config/config.go` 變更:
|
||||
|
||||
```go
|
||||
type ConversionConfig struct {
|
||||
ConverterBaseURL string `env:"VISIONA_CONVERTER_BASE_URL"`
|
||||
FAABaseURL string `env:"VISIONA_FAA_BASE_URL"`
|
||||
ConverterAPIKey string `env:"VISIONA_CONVERTER_API_KEY"` // Phase 0.8b 新增
|
||||
FAAAPIKey string `env:"VISIONA_FAA_API_KEY"` // Phase 0.8b 新增
|
||||
// Phase 0.8b 廢棄欄位(為 backward compat 保留 struct field 但 conversion 不再使用):
|
||||
// TenantID string `env:"VISIONA_OIDC_TENANT_ID"`
|
||||
}
|
||||
|
||||
func (c ConversionConfig) Enabled() bool {
|
||||
return c.ConverterBaseURL != "" &&
|
||||
c.FAABaseURL != "" &&
|
||||
c.ConverterAPIKey != "" &&
|
||||
c.FAAAPIKey != ""
|
||||
}
|
||||
```
|
||||
|
||||
`OIDCConfig.ServiceClientID` / `ServiceClientSecret` 兩個欄位 Phase 0.8b **conversion 不再依賴**;如其他模組未使用即可從 struct 移除。env 端從 `.env*.example` 移除以免誤導。
|
||||
|
||||
新增的 stage env:
|
||||
|
||||
```bash
|
||||
# .env.stage
|
||||
VISIONA_CONVERTER_BASE_URL=http://192.168.0.130:9501
|
||||
VISIONA_FAA_BASE_URL=http://192.168.0.130:5081
|
||||
VISIONA_CONVERTER_API_KEY=<openssl rand -hex 32 產的值,與 converter 端 CONVERTER_API_KEY 對齊>
|
||||
VISIONA_FAA_API_KEY=<openssl rand -hex 32 產的值,與 FAA 端 FAA_API_KEY 對齊>
|
||||
|
||||
# Phase 0.8b 移除:
|
||||
# VISIONA_OIDC_SERVICE_CLIENT_ID
|
||||
# VISIONA_OIDC_SERVICE_CLIENT_SECRET
|
||||
# VISIONA_OIDC_TENANT_ID(conversion 不依賴;其他模組未發現使用)
|
||||
```
|
||||
|
||||
### 3.3 啟動時驗證
|
||||
#### 3.1.2 啟動時驗證
|
||||
|
||||
api-server 啟動時 log 一行(**不可 log key 本身**):
|
||||
|
||||
```
|
||||
[INFO] conversion config: converter=http://192.168.0.130:9501 (api_key_set=true) faa=http://192.168.0.130:5081 (api_key_set=true)
|
||||
[INFO] conversion config: converter=http://192.168.0.130:9501 (api_key_set=true)
|
||||
```
|
||||
|
||||
若 `Enabled() == false` → conversion 模組整個 disabled(與 Phase 0.8 「partial deploy」相容,sidebar tab 仍會顯示但會回 503 `service_busy`)。
|
||||
|
||||
### 3.4 Key 產生 / 部署 / Rotate
|
||||
#### 3.1.3 Key 產生 / 部署 / Rotate
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| 長度 | 64 字元 hex(256 bit 熵) — `openssl rand -hex 32` |
|
||||
| 環境隔離 | dev / stage / prod 各自獨立的 key,**不重用** |
|
||||
| 兩個下游 | converter / FAA 各自一把,**不共用** |
|
||||
| 儲存(dev)| `.env.dev`(gitignore) |
|
||||
| 儲存(stage)| stage host `.env.stage`(不進 git) |
|
||||
| 儲存(prod)| AWS Secrets Manager / Vault |
|
||||
| Rotate | runbook Phase 0.9 補;流程:產新 key → 雙方同步 env → restart → 驗證 → 拔舊 key |
|
||||
| Log policy | 永遠不印 key 全文;可印 `api_key_set=true/false` 或前 8 字元 prefix |
|
||||
|
||||
### 3.5 Trust boundary 對齊
|
||||
### 3.2 ~~visionA → FAA~~(v0.6 整段撤回;改走 ADR-016 converter 中轉)
|
||||
|
||||
API key 證明的是「caller 是 visionA」(machine 身份);user_id 的真實性由 visionA 內部的 OIDC cookie session 保證(user 身份)。兩條獨立鏈:
|
||||
> **v0.6 整段撤回說明**:v0.5 在本節描述的「visionA → MC(issue service token + delegated download token)→ FAA」鏈路**是 fictional**——對 MC source 全 grep 驗證後確認 MC 沒有 `POST /file-access/download-tokens` endpoint、也沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint。
|
||||
>
|
||||
> **v0.6 採用**:visionA 端不再有任何 visionA → FAA / visionA → MC server-to-server 路徑。download / 加到模型庫 兩條 path 的 NEF 取得改走 visionA → converter `GET /api/v1/jobs/{id}/result`(用同一把 `VISIONA_CONVERTER_API_KEY`),converter 從自己的 MinIO stream NEF 回 visionA。
|
||||
>
|
||||
> 詳見 [ADR-016 §1 / §2](./adr/adr-016-download-via-converter.md)。
|
||||
>
|
||||
> **v0.5 本節原內容**(FAA dual-auth 設計、MC service client 配置、tenant_id claim 驗證等)**僅作歷史保留**——對應的 source code 已於 commit `86b7175` 移除(faa_client.go / mc_token_client.go 整檔砍除),v0.5 規劃的「mc_token_client 部分復活」決定也撤回(不需復活)。
|
||||
|
||||
- **machine auth**:visionA → converter / FAA 用 API key
|
||||
#### ~~3.2.1 FAA dual-auth 設計~~(v0.6 撤回 — 僅作歷史保留)
|
||||
|
||||
~~對應 [ADR-015 v2.0 §2](./adr/adr-015-server-to-server-api-key.md)~~(v0.6 整段撤回)。**v0.6 設計請看 [ADR-016](./adr/adr-016-download-via-converter.md)**。
|
||||
|
||||
#### ~~3.2.1 FAA dual-auth 設計~~(v0.6 撤回 — 僅作歷史保留說明為什麼 v0.5 路徑走不通)
|
||||
|
||||
FAA `/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` 既有設計(**Phase 0.8 / v2.0 都不動 FAA repo**):
|
||||
|
||||
| FAA endpoint | line range | auth 機制 | visionA 端 token type |
|
||||
|--------------|-----------|----------|----------------------|
|
||||
| `GET /files/metadata/{**objectKey}` | 80-111 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant`(scope `files:metadata.read` + tenant_id 驗)| MC service token |
|
||||
| `HEAD /files/{**objectKey}` | 113-148 | 同上 | MC service token |
|
||||
| `PUT /files/{**objectKey}` | 150-182 | `.RequireAuthorization()` + scope `files:upload.write` + tenant 驗 | MC service token |
|
||||
| **`GET /files/{**objectKey}`**(**下載**)| **184-254** | **無 `.RequireAuthorization()`;用 `IDelegatedDownloadTokenValidator.ValidateAsync(...)` 驗(active + tenant_id + object_key + method)**| **MC delegated download token** |
|
||||
| `DELETE /files/{**objectKey}` | 256-287 | `.RequireAuthorization()` + scope `files:delete` + tenant 驗 | MC service token |
|
||||
|
||||
→ **FAA `GET /files/{key}` 不接 service token,必須用 delegated download token**。
|
||||
→ visionA Phase 0.8 flow 只用 `GET /files/{key}`(加到模型庫 pull + 下載 stream proxy 兩條 path 都打這個 endpoint),所以**兩條 path 都走 delegated download token 路徑**。
|
||||
→ 其他 FAA endpoint(PUT / metadata / HEAD / DELETE)保留給 Phase 1+ 擴充用(如 visionA 主動清理 FAA 上的孤兒檔),到時候才走 service token 路徑。
|
||||
|
||||
#### ~~3.2.2 visionA 端流程~~(v0.6 撤回 — 整段不再執行)
|
||||
|
||||
```
|
||||
visionA-backend 啟動
|
||||
↓
|
||||
讀 cfg.OIDC.ServiceClientID / ServiceClientSecret(env VISIONA_OIDC_SERVICE_CLIENT_ID / _SECRET)
|
||||
讀 cfg.OIDC.IssuerURL(env VISIONA_OIDC_ISSUER_URL — 同 user login 的 issuer)
|
||||
讀 cfg.Conversion.TenantID(env VISIONA_OIDC_TENANT_ID)
|
||||
讀 cfg.Conversion.FAABaseURL(env VISIONA_FAA_BASE_URL)
|
||||
↓
|
||||
[Download 請求進來]
|
||||
↓
|
||||
flow.DownloadStream / flow.PromoteToModels:
|
||||
↓
|
||||
mcTokenClient.ServiceToken(ctx)
|
||||
cache hit?
|
||||
Yes → return cached token(exp - 15s 內)
|
||||
No → POST {issuer}/oauth/token
|
||||
grant_type=client_credentials
|
||||
client_id=<ServiceClientID>
|
||||
client_secret=<ServiceClientSecret>
|
||||
scope=files:upload.write files:metadata.read files:delete files:download.delegate
|
||||
→ cache + return token
|
||||
↓
|
||||
mcTokenClient.IssueDelegatedDownload(ctx, objectKey, "GET", 5*time.Minute)
|
||||
POST {issuer}/file-access/download-tokens
|
||||
Authorization: Bearer <service-token>
|
||||
Body: { object_key, method, ttl_seconds, tenant_id }
|
||||
→ DownloadGrant { token, expires_at, object_key, method }
|
||||
↓
|
||||
faaClient.DownloadWithDelegated(ctx, grant.Token, objectKey)
|
||||
GET {faaBaseURL}/files/{objectKey}
|
||||
Authorization: Bearer <delegated-token>
|
||||
→ io.ReadCloser + metadata
|
||||
↓
|
||||
(flow.go 內把 stream + filename + size 包成 DownloadMetadata 回 handler)
|
||||
```
|
||||
|
||||
#### ~~3.2.3 Config 對齊~~(v0.6 撤回 — visionA 端不需 ServiceClient* / TenantID / FAABaseURL)
|
||||
|
||||
`visionA-backend/internal/config/config.go` 變更(v2.0 修訂):
|
||||
|
||||
```go
|
||||
type ConversionConfig struct {
|
||||
ConverterBaseURL string `env:"VISIONA_CONVERTER_BASE_URL"`
|
||||
FAABaseURL string `env:"VISIONA_FAA_BASE_URL"`
|
||||
ConverterAPIKey string `env:"VISIONA_CONVERTER_API_KEY"` // v1.0 新增;v2.0 維持
|
||||
// FAAAPIKey 撤回(v1.0 加的,v2.0 移除)
|
||||
TenantID string `env:"VISIONA_OIDC_TENANT_ID"` // v1.x 廢棄;v2.0 重新啟用(FAA 線需要)
|
||||
}
|
||||
|
||||
// OIDCConfig.ServiceClientID / ServiceClientSecret 兩欄位 v2.0 重新啟用(v1.x 廢棄)
|
||||
type OIDCConfig struct {
|
||||
// ... user login 相關欄位(不變)...
|
||||
ServiceClientID string `env:"VISIONA_OIDC_SERVICE_CLIENT_ID"`
|
||||
ServiceClientSecret string `env:"VISIONA_OIDC_SERVICE_CLIENT_SECRET"`
|
||||
}
|
||||
|
||||
func (c ConversionConfig) Enabled() bool {
|
||||
return c.ConverterBaseURL != "" &&
|
||||
c.FAABaseURL != "" &&
|
||||
c.ConverterAPIKey != "" &&
|
||||
c.TenantID != ""
|
||||
// OIDCConfig.ServiceClientID / Secret 是否設好由 main.go 啟動時組合判斷
|
||||
}
|
||||
```
|
||||
|
||||
新增的 stage env(v2.0 修訂):
|
||||
|
||||
```bash
|
||||
# .env.stage
|
||||
# === user login(不變,沿用 oidc-tdd.md §13.1.1)===
|
||||
VISIONA_OIDC_ISSUER_URL=https://stage-9527.innovedus.com:7850/
|
||||
# ... user login 其他 env ...
|
||||
|
||||
# === Phase 0.8b v2.0 — converter 線 API key ===
|
||||
VISIONA_CONVERTER_BASE_URL=http://192.168.0.130:9501
|
||||
VISIONA_CONVERTER_API_KEY=<openssl rand -hex 32 產的值,與 converter 端 CONVERTER_API_KEY 對齊>
|
||||
|
||||
# === Phase 0.8b v2.0 — FAA 線 MC service token + delegated download token ===
|
||||
VISIONA_FAA_BASE_URL=https://stage-9527.innovedus.com:5081
|
||||
VISIONA_OIDC_SERVICE_CLIENT_ID=4242ba63099d4f318dd3f143d27ef4c5
|
||||
VISIONA_OIDC_SERVICE_CLIENT_SECRET=<see stage host .env.stage; 不進 git / 文件>
|
||||
VISIONA_OIDC_TENANT_ID=732270c0-449c-489c-bfad-321e9bf89b3d
|
||||
# Service scopes(由 MC service client 註冊時對應):
|
||||
# files:upload.write files:metadata.read files:delete files:download.delegate
|
||||
|
||||
# === Phase 0.8b v2.0 撤回 ===
|
||||
# VISIONA_FAA_API_KEY 撤回(v1.0 加的,v2.0 移除)
|
||||
```
|
||||
|
||||
> ⚠️ **secret 絕不寫進 git / 文件**:上方 `VISIONA_OIDC_SERVICE_CLIENT_SECRET` 真實值僅由使用者放 stage host 的 `.env.stage` 與部署 secret store;文件 / git 一律用 placeholder。
|
||||
|
||||
#### ~~3.2.4 啟動時驗證~~(v0.6 撤回 — visionA 端啟動不再 log FAA s2s config)
|
||||
|
||||
api-server 啟動時 log 一行(**不可 log secret / token**):
|
||||
|
||||
```
|
||||
[INFO] FAA s2s config: faa=https://stage-9527.innovedus.com:5081 service_client_set=true tenant_id_set=true
|
||||
```
|
||||
|
||||
若 OIDC ServiceClientID / Secret / Conversion TenantID / FAABaseURL 任一缺失 → conversion 模組 disabled(同 Phase 0.8「partial deploy」相容性)。
|
||||
|
||||
#### ~~3.2.5 MC scope 與 FAA endpoint 對應~~(v0.6 撤回 — visionA 端不再 issue MC service token,scope 配置由 converter 自己 / 不影響 visionA)
|
||||
|
||||
確認 MC service client `4242ba63099d4f318dd3f143d27ef4c5` 註冊時對應的 4 個 scope 完整覆蓋 FAA endpoint:
|
||||
|
||||
| FAA endpoint | 需要的 scope(在 service token 或 delegated token 上)| service client 是否備好 |
|
||||
|--------------|--------------------------------------------------|---------------------|
|
||||
| `PUT /files/...` | `files:upload.write` | ✅ |
|
||||
| `GET /files/metadata/...` + `HEAD` | `files:metadata.read` | ✅ |
|
||||
| `DELETE /files/...` | `files:delete` | ✅ |
|
||||
| `GET /files/{key}` 下載 | `files:download.delegate`(service token 用來向 MC 換 delegated download token;FAA 端最終驗的是 delegated token 本身,不是 scope)| ✅ |
|
||||
|
||||
→ Phase 0.8b v2.0 範圍內 visionA 只觸發「`GET /files/{key}` 下載」這條 path(加到模型庫 + 下載),所以 stage e2e 主要驗證 `files:download.delegate` 走通。其他 3 個 scope 為 Phase 1+ 預留。
|
||||
|
||||
**待 verify(合規性追蹤)**:stage redeploy 前實測 `POST /oauth/token` 拿到含 4 scope 的 access_token,並用該 token 對 MC `POST /file-access/download-tokens` 成功 issue delegated token。
|
||||
|
||||
### 3.3 Trust boundary 對齊(v0.6)
|
||||
|
||||
- **machine auth(visionA 端唯一一條)**:visionA → converter 用 pre-shared API key(init / poll / promote / **GetResult**)
|
||||
- **machine auth(不在 visionA 範圍)**:converter → FAA 用 OAuth client_credentials + `files:upload.write` scope(converter 自己管 `apps/task-scheduler/src/fileAccessAgent/client.js`、Phase 1 已上線)
|
||||
- **user auth**:browser → visionA 用 OIDC cookie session(既有,未變)
|
||||
- visionA 是橋樑:從 OIDC sub 解出 user_id → 透過 multipart body / API path 灌進對下游的請求
|
||||
- visionA 是橋樑:從 OIDC sub 解出 user_id → 透過 multipart body 灌進對 converter 的請求(init);對 download 路徑而言,visionA 端的 API key 證明「caller 是 visionA」、ownership store 確認 user_id 與 jobID 的綁定(converter 不重複驗 user-job 關係,因為 visionA 已驗)
|
||||
|
||||
詳見 §7。
|
||||
|
||||
@ -460,7 +619,7 @@ API key 證明的是「caller 是 visionA」(machine 身份);user_id 的
|
||||
| `POST` | `/api/conversion/init` | OIDC cookie | 上傳 + 建 job(multipart streaming) |
|
||||
| `GET` | `/api/conversion/{job_id}` | OIDC cookie | 查 job 狀態 |
|
||||
| `POST` | `/api/conversion/{job_id}/promote-to-models` | OIDC cookie | 「加到模型庫」 |
|
||||
| `GET` | `/api/conversion/{job_id}/download` | OIDC cookie | 「下載」— server-side HTTP 302 redirect 到 FAA delegated URL |
|
||||
| `GET` | `/api/conversion/{job_id}/download` | OIDC cookie | 「下載」— v0.6:server-side stream proxy;visionA backend 中轉 NEF binary(source 從 converter `GET /api/v1/jobs/{id}/result` 拉,使用 `VISIONA_CONVERTER_API_KEY`;不再經 FAA / MC) |
|
||||
| `GET` | `/api/conversion/active` | OIDC cookie | 查當前 user 有無 active job(frontend pre-check);in-memory miss 時 fallback 對 converter lazy rebuild(§2.6.1) |
|
||||
|
||||
> **不對外暴露但內部使用的 converter endpoint**:`GET /api/v1/jobs?user_id=&status=in_progress`(§2.6.1 lazy rebuild)。Phase 0.8 frontend 看不到「歷史列表」UI,但後端會用此內部 endpoint 做韌性處理。
|
||||
@ -474,9 +633,13 @@ API key 證明的是「caller 是 visionA」(machine 身份);user_id 的
|
||||
- response 用既有 `WriteSuccess` / `WriteError` helper
|
||||
- request_id 透傳給 converter(`X-Request-Id` header)
|
||||
|
||||
### 4.1 `GET /api/conversion/{job_id}/download` — Phase 0.8b:server-side stream proxy handler
|
||||
### 4.1 `GET /api/conversion/{job_id}/download` — Phase 0.8b v0.6:server-side stream proxy from converter
|
||||
|
||||
> **變更**:Phase 0.8(ADR-014 v1.1)原本是 `c.Redirect(302, FAA_URL_with_delegated_token)`;Phase 0.8b API key 模式下無 MC delegated token,改為 visionA backend 中轉 stream。
|
||||
> **演進**:
|
||||
> - **Phase 0.8(ADR-014 v1.1)**:`c.Redirect(302, FAA_URL_with_delegated_token)`
|
||||
> - **Phase 0.8b v0.4 (ADR-015 v1.x)**:改為 server-side stream proxy;token 來源用 visionA API key(v0.5 撤回)
|
||||
> - **Phase 0.8b v0.5 (ADR-015 v2.0)**:server-side stream proxy 保留;token 來源改回 MC delegated download token(**但對 MC source 驗證後確認此設計 fictional、未實際 e2e 跑通**)
|
||||
> - **Phase 0.8b v0.6 (ADR-016)**:server-side stream proxy 保留;stream 來源**從 FAA 改 converter `GET /api/v1/jobs/{id}/result`**;visionA 端不再經 MC / FAA
|
||||
|
||||
```go
|
||||
// GET /api/conversion/{job_id}/download
|
||||
@ -485,7 +648,13 @@ func conversionDownloadHandler(deps Deps) gin.HandlerFunc {
|
||||
uc, _ := UserContextFrom(c) // AuthMiddleware 已驗
|
||||
jobID := c.Param("job_id")
|
||||
|
||||
// service 內部完成:ownership 檢查 → ensurePromoted → 從 FAA pull stream
|
||||
// service 內部完成(v0.6 流程):
|
||||
// 1. ownership 檢查(visionA in-memory store)
|
||||
// 2. ensurePromoted(對 converter 冪等 promote,確保 converter MinIO 內有 NEF)
|
||||
// 3. converter.GetResult(ctx, jobID)
|
||||
// GET {ConverterBaseURL}/api/v1/jobs/{jobID}/result
|
||||
// Authorization: Bearer <ConverterAPIKey>
|
||||
// → 200 NEF binary stream + Content-Length + Content-Disposition
|
||||
stream, meta, err := deps.Conversion.DownloadStream(c.Request.Context(), uc.UserID, jobID)
|
||||
if err != nil {
|
||||
writeConversionError(c, err) // §6 錯誤碼分類
|
||||
@ -493,21 +662,24 @@ func conversionDownloadHandler(deps Deps) gin.HandlerFunc {
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
// streaming proxy 給 client(io.Copy;不暫存 disk / RAM 全 buffer)
|
||||
// streaming proxy 給 client(io.CopyN;不暫存 disk / RAM 全 buffer)
|
||||
c.Header("Content-Type", meta.ContentType)
|
||||
if meta.SizeBytes > 0 {
|
||||
c.Header("Content-Length", strconv.FormatInt(meta.SizeBytes, 10))
|
||||
}
|
||||
// 鼓勵 browser 觸發 save dialog
|
||||
// 注意:meta.Filename **不是** FAA metadata 直接給的(FAA 端的 object_key 是
|
||||
// `models/<user>/<job>.nef` 對 user 不友善),而是 visionA backend 在 service 層
|
||||
// 由 `defaultDownloadFilename(cj)` 從 conversion job metadata 構造,規則:
|
||||
// 注意:meta.Filename **不是** converter 直接給的 raw object_key(converter 端的
|
||||
// object_key 是 `models/<user>/<job>.nef` 對 user 不友善);converter response 的
|
||||
// Content-Disposition 雖含 filename 建議值,但 visionA backend 仍在 service 層用
|
||||
// `defaultDownloadFilename(cj)` 從 conversion job metadata 重新構造,規則:
|
||||
// `<source_filename_stem>_<target_chip_lower>.nef`(例:`yolov5s_kl720.nef`),
|
||||
// 對齊 wireframe success card 顯示範例(`yolov5s.onnx → yolov5s_kl720.nef`)。
|
||||
c.Header("Content-Disposition", `attachment; filename="`+sanitizeFilename(meta.Filename)+`"`)
|
||||
c.Header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
c.Status(http.StatusOK)
|
||||
io.Copy(c.Writer, stream)
|
||||
// 注意:用 io.CopyN(c.Writer, stream, sizeCap) 帶 1 GiB 上限保護 visionA backend 不被超大檔吃記憶體
|
||||
// sizeCap = 1 << 30(1 GiB)— 對 Phase 0.8 NEF(通常 < 100MB)寬鬆但有上限
|
||||
io.CopyN(c.Writer, stream, 1<<30)
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -518,7 +690,7 @@ func conversionDownloadHandler(deps Deps) gin.HandlerFunc {
|
||||
- GET semantically 對應「拿一個資源」,符合「下載這個 job 的結果」語意
|
||||
- 既有 OIDC AuthMiddleware 會自然處理 GET 上的 cookie session,無 CSRF 風險(沒有狀態變更,promote 是冪等的)
|
||||
|
||||
**Frontend 使用範例**(與 Phase 0.8 一致,無需改動):
|
||||
**Frontend 使用範例**(與 Phase 0.8 / v0.4 一致,無需改動):
|
||||
|
||||
```html
|
||||
<!-- 推薦:anchor tag,browser 自動處理 navigation + 收 attachment -->
|
||||
@ -532,17 +704,23 @@ func conversionDownloadHandler(deps Deps) gin.HandlerFunc {
|
||||
window.location.href = `/api/conversion/${jobId}/download`;
|
||||
```
|
||||
|
||||
**安全性面比較(Phase 0.8 → Phase 0.8b)**:
|
||||
**安全性面比較(Phase 0.8 → v0.4 → v0.5 → v0.6)**:
|
||||
|
||||
| 面向 | Phase 0.8(302 redirect + MC delegated token)| Phase 0.8b(server-side stream proxy)|
|
||||
|------|----|----|
|
||||
| Token 在 frontend JS / URL bar | △ 短暫(Location header 流經 browser,nav 完即消失) | ✓ 結構性不存在(無 token 概念)|
|
||||
| 要 FAA CORS | ✓ 不需要(navigation 不適用 CORS)| ✓ 同 — visionA 為 same-origin,FAA 直連在 server-side |
|
||||
| 跨 internet 流量(同 NEF 多次下載)| ✓ 直連 FAA、N× 流量算 FAA 出 | ✗ 每次都繞 visionA backend,N× 流量算 visionA 出 |
|
||||
| visionA backend 是否變 streaming bottleneck | ✓ 不是 | ✗ 是 — Phase 0.8 MVP user 量小可接受;Phase 1 量大需改 ADR-015 §7 選項 B |
|
||||
| 認證鏈簡化 | ✗ 需要 MC scope `files:download.delegate` | ✓ 一把 API key 解決 |
|
||||
| 面向 | Phase 0.8(302 + MC delegated token)| v0.4(server-side proxy + visionA API key 撤回)| v0.5(server-side proxy + MC delegated token;**fictional 從未跑通**)| **v0.6(server-side proxy + visionA → converter API key)** |
|
||||
|------|----|----|---|---|
|
||||
| Token 在 frontend JS / URL bar | △ 短暫(Location header 流經 browser) | ✓ 結構性不存在 | ✓ 結構性不存在 | ✓ 結構性不存在(API key 只在 server-side 流動)|
|
||||
| 要 FAA CORS | ✓ 不需要 | ✓ 不需要 | ✓ 不需要 | ✓ 不需要(visionA 端不直接打 FAA、CORS 完全不適用) |
|
||||
| 跨 internet 流量(同 NEF 多次下載)| ✓ 直連 FAA | ✗ 每次繞 visionA | ✗ 每次繞 visionA | ✗ 每次繞 visionA(同 v0.4 / v0.5,未改變;但 source 從 FAA 換 converter MinIO)|
|
||||
| visionA backend 是否變 streaming bottleneck | ✓ 不是 | ✗ 是 | ✗ 是 | ✗ 是(同 v0.4 / v0.5,Phase 0.8b MVP 接受;Phase 1+ 升級見 ADR-016 後果 §負面影響)|
|
||||
| 認證鏈複雜度(visionA → 下游)| MC service token + MC delegated token | 一把 API key | MC service token + MC delegated token(fictional)| 一把 API key(同 v0.4,但這次是真的 work)|
|
||||
| Token TTL | 5 min(MC 簽)| ∞(API key long-lived)| 5 min(MC 簽,但 endpoint 不存在所以 issue 不到)| ∞(API key long-lived;rotate by runbook)|
|
||||
| Token 洩漏的 blast radius | 5 min 內可下載該 object_key | 永遠可打 FAA 任何 endpoint | — | 永遠可打 converter 任何 endpoint(jimchen 自己管 rotate;converter 不存其他 user 資料、攻擊面限於 converter 自己)|
|
||||
|
||||
**Phase 1 升級路徑**:如量大需回 302 redirect 模式,採 ADR-015 §7 選項 B(visionA 自己簽 short-TTL HMAC token,FAA middleware 多支援「visionA HMAC」路徑)。
|
||||
**為什麼 v0.6 仍對齊 v0.4 / v0.5 的 server-side proxy 而非退回 302**:見 §1 整體 flow 變更說明。
|
||||
|
||||
**Phase 1+ 升級路徑**:如量大需回 302 redirect 模式(讓 browser 直連 converter 或 FAA),有兩個方向:
|
||||
- 方向 A(converter Phase 2):converter 補上 `POST /api/v1/jobs/:id/download-tokens`(既有預留 501),給 browser 直連 converter;ADR-016 與此路徑相容
|
||||
- 方向 B(FAA HMAC):visionA 自己簽 short-TTL HMAC token + FAA middleware 加第三條 auth path(同 ADR-015 v2.0 §7 選項 B);但需要 warrenchen 改公司共用 FAA repo
|
||||
|
||||
---
|
||||
|
||||
@ -785,38 +963,47 @@ converter multer 偵測 incomplete multipart → 拒絕收 job(不會建 job_i
|
||||
|
||||
## 6. 錯誤碼 mapping + i18n key
|
||||
|
||||
> **Phase 0.8b 變更**:移除所有「MC token」相關錯誤碼(`idp_misconfigured` / `idp_unavailable` / `download_token_failed` / `mc_token_unavailable`)— 服務間認證已不再經 MC。新增「API key 驗證失敗」錯誤碼(visionA 端不直接面對,但下游若回 401 要處理)。
|
||||
> **Phase 0.8b v0.6 變更**:撤回 v0.5「回收 MC 兩個 code」決定。visionA 端不再有 MC / FAA 直接呼叫、`mc_token_unavailable` / `download_token_failed` 兩個 code 移除。新增 converter result endpoint 的 `result_not_found` / `result_expired` 兩個 code。
|
||||
|
||||
| Converter / FAA error | visionA error code | HTTP | i18n key | user-friendly 訊息(zh-TW) |
|
||||
| Converter error | visionA error code | HTTP | i18n key | user-friendly 訊息(zh-TW) |
|
||||
|----------|--------------------|------|----------|--------------------------|
|
||||
| converter `validation_error` | `validation_failed` | 400 | `conversion.error.validation` | 上傳的檔案不符合要求(請查看詳細欄位錯誤) |
|
||||
| converter `invalid_multipart` | `validation_failed` | 400 | `conversion.error.invalid_multipart` | 上傳格式錯誤,請重新嘗試 |
|
||||
| converter `user_has_active_job` | `active_job_exists` | 409 | `conversion.error.active_job` | 你目前已有進行中的轉檔任務 |
|
||||
| converter `file_too_large` | `payload_too_large` | 413 | `conversion.error.too_large` | 檔案超過大小限制 |
|
||||
| converter `service_busy` | `service_busy` | 503 | `conversion.error.busy` | 系統繁忙,請稍後再試 |
|
||||
| converter `storage_unavailable` | `converter_unavailable` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用 |
|
||||
| converter `storage_unavailable`(MinIO 不可達)| `converter_unavailable` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用 |
|
||||
| converter 5xx / network | `converter_unavailable` | 502 | `conversion.error.converter_down` | 同上 |
|
||||
| **converter 401(API key 不對 / 過期 / rotate 未同步)** | `converter_auth_failed` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用(內部 log 區分 auth_failed vs 5xx)|
|
||||
| FAA 5xx / network | `faa_unavailable` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用 |
|
||||
| **FAA 401(API key 不對 / 過期 / rotate 未同步)** | `faa_auth_failed` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用(內部 log 區分 auth_failed vs 5xx)|
|
||||
| job 不屬於當前 user | `forbidden` | 403 | `conversion.error.forbidden` | 你無權存取此轉檔任務 |
|
||||
| job_id 不存在 | `not_found` | 404 | `conversion.error.not_found` | 轉檔任務不存在 |
|
||||
| **converter 404 `result_not_found`**(v0.6 新增;`GET /api/v1/jobs/{id}/result` job 不存在)| `not_found` | 404 | `conversion.error.not_found` | 轉檔任務不存在 |
|
||||
| **converter 410 `result_expired`**(v0.6 新增;job completed 但 NEF 已被 converter MinIO GC、超 7 天 expires_at)| `result_expired` | 410 | `conversion.error.result_expired` | 轉檔結果已過期,請重新轉檔 |
|
||||
| job 不屬於當前 user(visionA 端 ownership 檢查)| `forbidden` | 403 | `conversion.error.forbidden` | 你無權存取此轉檔任務 |
|
||||
| job_id 不存在(visionA ownership store) | `not_found` | 404 | `conversion.error.not_found` | 轉檔任務不存在 |
|
||||
| job 還沒 completed | `job_not_completed` | 409 | `conversion.error.not_completed` | 任務尚未完成,請等轉檔完成再下載 |
|
||||
|
||||
**Phase 0.8b 移除的錯誤碼**(與 MC token 相關,認證路徑取消後不會發生):
|
||||
**v0.6 變更摘要**(相對於 v0.5):
|
||||
|
||||
| 已移除 | 原來語意 | Phase 0.8b 替代 |
|
||||
|------|----------|-----|
|
||||
| `idp_misconfigured`(500) | MC token endpoint 回 4xx(scope 沒註冊 / client 設錯)| — |
|
||||
| `idp_unavailable`(503) | MC token endpoint 5xx | — |
|
||||
| `download_token_failed`(502) | MC delegated token 4xx | — |
|
||||
| `mc_token_unavailable`(502) | MC 持續失敗 | — |
|
||||
| code | v0.5 狀態 | v0.6 狀態 | 說明 |
|
||||
|------|---------|---------|------|
|
||||
| `converter_auth_failed` | 維持 | **維持** | converter API key 仍使用(init / poll / promote / GetResult 共用同一把)|
|
||||
| `converter_unavailable` | 維持 | **維持** | converter 5xx / network |
|
||||
| `result_not_found` | — | **新增** | converter `GET /api/v1/jobs/{id}/result` 回 404 |
|
||||
| `result_expired` | — | **新增** | converter `GET /api/v1/jobs/{id}/result` 回 410(job 過期)|
|
||||
| `faa_unavailable` | 使用中 | **撤回** | visionA 端不再直接打 FAA |
|
||||
| `mc_token_unavailable` | 回收 | **撤回** | visionA 端不再打 MC |
|
||||
| `download_token_failed` | 回收 | **撤回** | visionA 端不再 issue delegated token |
|
||||
| ~~`faa_auth_failed`~~ | v0.5 撤回 | 維持撤回 | (v0.4 短暫存在)|
|
||||
| ~~`idp_misconfigured`~~ / ~~`idp_unavailable`~~ | 維持移除 | 維持移除 | — |
|
||||
|
||||
下游 401 對待原則:
|
||||
下游錯誤對待原則(v0.6):
|
||||
|
||||
- 對 visionA 端而言,下游 401 是「**部署設定錯誤**」(API key 不對齊)— 跟「使用者沒登入」(visionA → frontend 401)完全不同層次
|
||||
- visionA 從 `converter_client` / `faa_client` 收到 401 → log error(含 request_id,方便 SRE 排查)→ 回 frontend `502 converter_auth_failed` / `faa_auth_failed`,不要對 frontend 暴露「API key 不對」這個內部細節
|
||||
- 401 不 retry(同 §9)— rotate 流程不同步是運維事件,需人工介入
|
||||
- **converter 401**(API key 不對齊)→ `converter_auth_failed`,內部 log 標 reason 給 SRE
|
||||
- **converter 404**(job_id 不存在 / 已被 GC)→ `result_not_found`,frontend 顯示「轉檔任務不存在」
|
||||
- **converter 410**(job completed 但 NEF 已過 7 天 expires_at 被 GC)→ `result_expired`,frontend 顯示「轉檔結果已過期,請重新轉檔」並提供重新轉檔 CTA
|
||||
- **converter 4xx 其他** → 透傳 + log
|
||||
- **converter 5xx / network** → `converter_unavailable`,retry 後仍失敗才回 frontend
|
||||
- 對 frontend 不暴露內部細節(API key 不對 / converter MinIO 問題 / 其他下游差異)—— 統一 user-friendly 文字 `轉檔服務暫時無法使用`(除 410 `result_expired` 給更精確的「過期」訊息)
|
||||
- 401 / 4xx 不 retry(同 §9)—— 都是運維事件或 user 端問題,需人工介入 / 重新轉檔
|
||||
|
||||
i18n key 命名:`conversion.error.<short-name>`,前端 i18n 字典在 `visionA-frontend/messages/{zh-TW,en}.json` 補。
|
||||
|
||||
@ -883,20 +1070,25 @@ frontend 用 `<a href>` 觸發時,若失敗 browser 會把錯誤頁顯示在
|
||||
|
||||
### 9.1 retry 規則
|
||||
|
||||
> **Phase 0.8b 變更**:移除 MC 兩 row;下游 401/403(API key 不對)一律不 retry。
|
||||
> **Phase 0.8b v0.6 變更**:撤回 v0.5「回收 MC 兩 row」決定。visionA 端不再有 MC / FAA 直接呼叫、相關 row 全部移除。新增 converter `GET /jobs/{id}/result` row。
|
||||
|
||||
| 操作 | 401 / 403 | 4xx 其他 | 5xx | network / timeout | max retry | 退避 |
|
||||
|------|-----------|---------|-----|------------------|-----------|------|
|
||||
| Converter `POST /jobs` | **不重試**(auth_failed)| 透傳 | retry | retry | 2 | 1s, 2s |
|
||||
| Converter `POST /jobs` | **不重試**(converter_auth_failed)| 透傳 | retry | retry | 2 | 1s, 2s |
|
||||
| Converter `GET /jobs/{id}` | **不重試** | 透傳 | retry | retry | 3 | 0.5s, 1s, 2s |
|
||||
| Converter `POST /jobs/{id}/cancel`(內部 cleanup,Phase 1+)| 不重試 | 不重試 | 不重試 | 不重試 | 0 | best-effort(失敗只 log);**Phase 0.8 converter 未實作此 endpoint,靠 socket close 兜底**(§5.3.2)|
|
||||
| Converter `GET /jobs?user_id=&status=in_progress`(lazy rebuild)| **不重試** | 透傳 | retry | retry | 1 | 0.5s |
|
||||
| Converter `POST /promote` | **不重試** | 透傳 | retry | retry | 2 | 1s, 2s |
|
||||
| FAA `GET /files/{key}`(s2s download / s2s pull) | **不重試** | 透傳 | retry | retry | 2 | 1s, 2s |
|
||||
| **Converter `GET /jobs/{id}/result`**(v0.6 新增;下載 NEF stream)| **不重試**(401 → converter_auth_failed;404 → result_not_found;410 → result_expired)| 透傳 | retry | retry | 2 | 1s, 2s |
|
||||
|
||||
每次 retry 之間檢查 `ctx.Done()`;ctx cancel → 立即 return ctx.Err()。
|
||||
|
||||
**401 不重試的理由**:API key 是 long-lived secret,rotate 同步是運維事件、不是瞬時抖動。401 通常意味「visionA env 與下游 env 不同步」,retry 100 次也不會自己變對,反而浪費 latency 並掩蓋運維事件。直接回 502 `*_auth_failed` 讓 SRE 看到。
|
||||
**401 / 403 不重試的理由**:
|
||||
|
||||
- **converter 401**:API key 是 long-lived secret,rotate 同步是運維事件、不是瞬時抖動。401 通常意味「visionA env 與下游 env 不同步」,retry 100 次也不會自己變對。直接回 502 `converter_auth_failed` 讓 SRE 看到。
|
||||
- **converter 404 / 410**:job_id 不存在(已 GC)或結果過期,是 user 端狀態問題;retry 不會讓 NEF 重新出現。
|
||||
|
||||
> **v0.5 §9.1.1「MC service token cache miss / FAA delegated token 過期」整段撤回**(v0.6 visionA 端不再有 MC / FAA 直接互動,失敗恢復路徑簡化為「converter 5xx retry max 2 次」單條規則)。
|
||||
|
||||
### 9.2 graceful degradation
|
||||
|
||||
@ -905,10 +1097,12 @@ frontend 用 `<a href>` 觸發時,若失敗 browser 會把錯誤頁顯示在
|
||||
| converter 完全不可達(持續 5xx) | `502 converter_unavailable`,UI 提示「轉檔服務暫時無法使用,請稍後再試」 |
|
||||
| converter 回 401(API key 不同步)| `502 converter_auth_failed`,UI 同上文字;SRE 從 log 看到 auth_failed 計數異常 → 檢查 env |
|
||||
| 完成後 promote 失敗(converter 5xx) | job 留在 completed 狀態(FAA 上沒檔但 visionA 知道),UI 給 user 「重試 promote」按鈕(重打 promote-to-models / download) |
|
||||
| FAA pull 失敗 — 加到模型庫流程(5xx)| model record 不寫入;UI 提示重試 |
|
||||
| FAA pull 失敗 — 下載流程(5xx)| visionA backend 中轉時 503 / 502,UI 提示重試(Phase 0.8b 兩條 download path 都共用 visionA → FAA pull)|
|
||||
| FAA 回 401(API key 不同步)| `502 faa_auth_failed`,UI 文字「檔案存取服務暫時無法使用」;SRE 從 log 排查 |
|
||||
| visionA-backend 重啟 | in-memory ownership 與 promoted_key cache 全失,frontend 進 /conversion 時 `/active` lazy rebuild(§2.6.1);rebuild 不到的 job 由 converter 7 天 expire 自然兜底 |
|
||||
| converter `GET /jobs/{id}/result` 回 404 `result_not_found`(v0.6 新增)| `404 result_not_found`,UI 顯示「轉檔任務不存在」 |
|
||||
| converter `GET /jobs/{id}/result` 回 410 `result_expired`(v0.6 新增)| `410 result_expired`,UI 顯示「轉檔結果已過期,請重新轉檔」並提供重新轉檔 CTA |
|
||||
| converter `GET /jobs/{id}/result` 5xx(converter MinIO 故障 / converter 自身 down)| `502 converter_unavailable`,UI 提示重試;SRE 從 log 看 5xx body 排查 |
|
||||
| visionA-backend 重啟 | in-memory ownership 與 promoted_key cache 全失,frontend 進 /conversion 時 `/active` lazy rebuild(§2.6.1);rebuild 不到的 job 由 converter 7 天 expire 自然兜底;**v0.6 不再有 MC service token cache(已刪除),cold start 沒有對應 latency**|
|
||||
|
||||
> **v0.5「FAA pull 失敗 / FAA 401 / 403 / MC token 失敗 / MC delegated token 失敗」整段 row 撤回**(v0.6 visionA 端不再有對應路徑)。
|
||||
|
||||
### 9.3 同 user active job 衝突
|
||||
|
||||
@ -932,46 +1126,51 @@ frontend 用 `<a href>` 觸發時,若失敗 browser 會把錯誤頁顯示在
|
||||
|
||||
詳見 §7。任何繞過此原則的設計都必須先過 ADR review。
|
||||
|
||||
### 10.2 ~~Delegated download token TTL~~(Phase 0.8b 移除)
|
||||
### ~~10.2 Delegated download token TTL~~(v0.6 整段撤回 — visionA 端不再有 delegated token)
|
||||
|
||||
> Phase 0.8b 不再有 delegated download token;download 走 server-side stream proxy。原段落(5 分鐘 TTL、`VISIONA_FAA_DELEGATED_TTL_SECONDS` env)刪除。
|
||||
>
|
||||
> Phase 1+ 若量大改 ADR-015 §7 選項 B(visionA 自簽 HMAC token),那時再回設 TTL 規格。
|
||||
> v0.4 移除(API key 模式下無 delegated token);v0.5 撤回 v0.4 並回收(FAA 線回到 MC delegated download token);**v0.6 再次整段撤回**——對 MC source 驗證後確認 MC 沒有 issue delegated token 的 endpoint,v0.5 設計是 fictional。visionA 端 v0.6 起完全沒有 delegated token 概念。
|
||||
|
||||
### 10.3 Pre-shared API key 保護(取代 Service token 保護)
|
||||
### 10.3 Pre-shared API key 保護(v0.6 仍縮限至 converter)
|
||||
|
||||
- `VISIONA_CONVERTER_API_KEY` / `VISIONA_FAA_API_KEY` 不可進 git(既有 `.gitignore` 含 `.env*`,配合 `!.env*.example`)
|
||||
- `VISIONA_CONVERTER_API_KEY` 不可進 git(既有 `.gitignore` 含 `.env*`,配合 `!.env*.example`)
|
||||
- 部署用 AWS Secrets Manager / k8s Secret 注入
|
||||
- log 永遠不印 key 全文;可印 `api_key_set=true` 或前 8 字元 prefix(debug 用)
|
||||
- 若 key 洩漏:產新 key → 雙方同步 env → restart visionA / converter(或 FAA)→ 驗證 → 拔舊 key(runbook Phase 0.9 補)
|
||||
- 已洩漏的 stage service client secret `RciRUyi...` 改 API key 後直接作廢,無 rotate 動作
|
||||
- 若 key 洩漏:產新 key → visionA + converter 同步 env → restart → 驗證 → 拔舊 key(runbook Phase 0.9 補)
|
||||
- v0.4 加的 `VISIONA_FAA_API_KEY` 在 v0.5 / v0.6 維持撤回
|
||||
- **v0.6 新增**:同一把 `VISIONA_CONVERTER_API_KEY` 也用於 download 路徑(converter `GET /api/v1/jobs/{id}/result`),不需新增 secret
|
||||
|
||||
### 10.4 Object key 不暴露給 frontend JS
|
||||
### ~~10.4 MC Service Token + Delegated Download Token 保護~~(v0.6 整段撤回 — visionA 端不再有 MC service token / delegated token)
|
||||
|
||||
- Phase 0.8b:visionA-backend 透過 server-side stream proxy 把 NEF stream 中轉回 browser,**FAA URL / object_key 都不出現在任何 frontend response**
|
||||
- frontend JS 對 object_key / 內部 FAA 路徑完全沒有 reference
|
||||
> v0.5 新增本節給 FAA 線;**v0.6 整段撤回**——visionA 端不再有 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` / `VISIONA_OIDC_TENANT_ID` 三個 env,不再 issue MC service token、不再 issue delegated download token。user login 的 OIDC(public PKCE)是另一條完全獨立的鏈、不在本節範圍(詳見 `oidc-tdd.md`)。
|
||||
|
||||
### 10.5 Object key 不暴露給 frontend JS
|
||||
|
||||
- Phase 0.8b v0.6:visionA-backend 透過 server-side stream proxy 把 NEF stream 中轉回 browser,**converter MinIO object_key / converter API key 都不出現在任何 frontend response**
|
||||
- frontend JS 對 object_key / 內部 converter 路徑完全沒有 reference
|
||||
- 防快取:handler 設 `Cache-Control: no-store, no-cache, must-revalidate`,避免 browser cache NEF stream
|
||||
- **不需 FAA CORS**:visionA → FAA 是 server-side 同進程內 outbound HTTP call,不適用 CORS(CORS 只管 browser JS fetch / XHR)
|
||||
- **不需 CORS(converter 端或 FAA 端)**:visionA → converter 是 server-side 同進程內 outbound HTTP call,不適用 CORS(CORS 只管 browser JS fetch / XHR);browser 完全不知道 converter / FAA 的存在
|
||||
- visionA backend 是 attack surface:任何能拿到 visionA cookie session 的 attacker 都能下載自己 user_id 的 NEF — 但這本來就是 user 自己的檔,無 escalation
|
||||
|
||||
### 10.4b Phase 0.8 → Phase 0.8b 安全面遷移摘要
|
||||
### 10.6 Phase 0.8 → v0.4 → v0.5 → v0.6 安全面遷移摘要
|
||||
|
||||
| 面向 | Phase 0.8(302 + delegated token)| Phase 0.8b(server-side proxy)|
|
||||
|------|----|----|
|
||||
| Token 結構是否存在 | 是(MC issue,5 分鐘 TTL)| 否 |
|
||||
| 攻擊者攔截 visionA → browser response 拿到 token | 短期可用 5 分鐘 | 結構性無 token |
|
||||
| Frontend XSS 影響範圍 | 短 TTL token | 無 token 可竊 |
|
||||
| Server compromise(visionA backend 被攻破)| 攻擊者可簽任意 MC delegated token | 攻擊者拿到 API key 後可任意打 converter / FAA |
|
||||
| Defense in depth | Token TTL + scope 限制 | API key + visionA OIDC 上游 user auth |
|
||||
| 結論 | 兩者都安全可接受;Phase 0.8b 取捨「實作簡化 + bottleneck」換「無 token in browser 的更乾淨模型」|
|
||||
| 面向 | Phase 0.8(302 + delegated token)| v0.4(server-side proxy + visionA API key)| v0.5(server-side proxy + delegated token;**fictional 從未跑通**)| **v0.6(server-side proxy + visionA → converter API key)**|
|
||||
|------|----|----|---|---|
|
||||
| Token 結構是否存在於 visionA ↔ 下游鏈 | 是(MC issue,5 分鐘 TTL,過 browser)| 是(visionA API key,long-lived,server-side only)| 是(MC issue,但實際 issue 不到)| 是(visionA API key,long-lived,server-side only)|
|
||||
| 攻擊者攔截 visionA → browser response 拿到 token | 短期可用 5 分鐘 | 結構性無 token | — | 結構性無 token(API key 不過 browser)|
|
||||
| Frontend XSS 影響範圍 | 短 TTL token | 無 token 可竊 | — | 無 token 可竊 |
|
||||
| Server compromise(visionA backend 被攻破)| 攻擊者可簽任意 MC delegated token(限於 service client 的 scope)| 攻擊者拿到 visionA API key 後可任意打 FAA 所有 endpoint | — | 攻擊者拿到 `VISIONA_CONVERTER_API_KEY` 後可任意打 converter 所有 endpoint;但 converter **不存其他 user / 其他產品線資料**(只存進行中 / 完成的轉檔 job、7 天 GC),blast radius 比 v0.4 直接打 FAA 小 |
|
||||
| MC 是否為依賴 | 是(issue token)| 否 | 是(issue service token + delegated token)| 否(visionA 端 server-to-server 不依賴 MC;user login 仍依賴 MC OIDC,但與本表無關)|
|
||||
| FAA 是否為依賴 | 是(直接打)| 是(直接打)| 是(直接打)| **否**(visionA 端只打 converter;converter → FAA 是 converter 自己的事)|
|
||||
| Defense in depth | Token TTL + scope 限制 + tenant 限制 | API key + visionA OIDC 上游 user auth | — | API key + visionA OIDC 上游 user auth + ownership store + converter MinIO 7 天 GC(自然 retention) |
|
||||
| 結論 | Phase 0.8 安全靠 MC token TTL;v0.4 移除 token 結構但 API key long-lived;v0.5 設計上應更安全但實際 fictional;**v0.6 與 v0.4 等價安全模型,但 blast radius 限於 converter(非 FAA)+ 失敗模式收斂為單條鏈,整體 SRE 可運維性最佳**|
|
||||
|
||||
### 10.5 Race condition
|
||||
### 10.7 Race condition
|
||||
|
||||
- 同 user 同時兩 tab init → 第一個成功寫 ownership / converter 接受;第二個 pre-check 通過但 converter 409
|
||||
- 兩 tab 同時 promote-to-models → 第一個寫 model record 成功;第二個重複呼叫 ensurePromoted(cache hit)→ FAA pull 兩次(接受的取捨;FAA 端冪等)→ models repo 寫入時可能撞 model_id 衝突 — 改用 model_id 在 finalize 前 SELECT 檢查
|
||||
- 兩 tab 同時 download → visionA backend 各自獨立 FAA pull(無 cache);兩條 stream 同時跑、兩條都成功(FAA 端冪等讀)— Phase 0.8b 可接受,量大時再加 server-side stream cache
|
||||
- 兩 tab 同時 promote-to-models → 第一個寫 model record 成功;第二個重複呼叫 ensurePromoted(cache hit)→ **converter.GetResult 拉 NEF 兩次**(接受的取捨;converter MinIO 端冪等讀)→ models repo 寫入時可能撞 model_id 衝突 — 改用 model_id 在 finalize 前 SELECT 檢查
|
||||
- 兩 tab 同時 download → visionA backend 各自獨立 converter.GetResult(無 cache);兩條 stream 同時跑、兩條都成功(converter MinIO 端冪等讀)— Phase 0.8b 可接受,量大時再加 server-side stream cache 或方向 A(converter Phase 2 download-tokens 讓 browser 直連 converter)
|
||||
|
||||
### 10.6 DoS 防護(最小集,Phase 1 強化)
|
||||
### 10.8 DoS 防護(最小集,Phase 1 強化)
|
||||
|
||||
- 同 user 1 個 active job 的限制本身就是 DoS 防護(user 不能 init 1000 個 job)
|
||||
- visionA-backend conversion endpoint 不額外 rate limit(Phase 1 補;對齊 `security.md` §4)
|
||||
@ -983,26 +1182,73 @@ frontend 用 `<a href>` 觸發時,若失敗 browser 會把錯誤頁顯示在
|
||||
|
||||
實作此 spec 會動到的檔案(給 Backend Agent 參考;Backend 自己拆任務):
|
||||
|
||||
**Phase 0.8b 變更(在 Phase 0.8 已上的 code 上面動)**:
|
||||
**Phase 0.8b v0.6 變更(相對於 v0.5 規劃但未上的 code,backend agent 下次任務範圍)**:
|
||||
|
||||
- 砍:`visionA-backend/internal/conversion/mc_token_client.go`(~440 行整檔刪除)
|
||||
- 砍:`visionA-backend/internal/conversion/mc_token_client_test.go`(對應 test)
|
||||
- 改:`visionA-backend/internal/conversion/converter_client.go` — 移除 `tokens *MCTokenClient` 欄位,改 `apiKey string`;每個 method 內 `Authorization: Bearer <apiKey>` 直接 set
|
||||
- 改:`visionA-backend/internal/conversion/faa_client.go` — 同上模式
|
||||
- 改:`visionA-backend/internal/conversion/flow.go` — 移除 `tokens` 欄位;download path 從 `DownloadRedirectURL` 改為 `DownloadStream`(從 FAA pull stream 回給 caller)
|
||||
- 改:`visionA-backend/internal/conversion/conversion.go` — `Service` interface `DownloadRedirectURL` 改為 `DownloadStream(...) (io.ReadCloser, *DownloadMetadata, error)`
|
||||
- 改:`visionA-backend/internal/api/conversion.go` — `conversionDownloadHandler` 從 `c.Redirect(302, ...)` 改為 `io.Copy(c.Writer, stream)` + 設好 Content-Type / Content-Disposition / Cache-Control
|
||||
> v0.4 階段已上線的 commit `86b7175` 把 converter / FAA 兩條線都改為 API key(並把 mc_token_client.go 整檔砍除)。v0.5 規劃要把 FAA 線回退、mc_token_client 部分復活(**但這部分尚未進實作**)。v0.6 撤回 v0.5 規劃,改為:commit `86b7175` 的 mc_token_client 砍除狀態**維持**;faa_client.go 改名為 converter_result_client.go(或併入 converter_client.go)承接新 `GetResult` method;config.go / .env 撤回 v0.5 規劃要加回的 OIDC ServiceClient* / TenantID / FAABaseURL。
|
||||
>
|
||||
> 同時:**converter 跨 repo 加新 endpoint**(jimchen 自己處理)。
|
||||
|
||||
**v0.6 跨 repo:converter scheduler(jimchen 在 `/Users/jimchen/kneron_model_converter/apps/task-scheduler/`)**:
|
||||
|
||||
- 新增 `src/routes/v1/result.js`(或加進 `jobs.js`):`GET /api/v1/jobs/:id/result` handler,套用既有 `requireReadAuth` middleware(API key 比對)
|
||||
- handler 內:job status / expires_at 檢查 → MinIO get object stream → `pipeline(minioStream, res)` + 設 headers
|
||||
- 新增 integration test:`src/routes/v1/__tests__/result.integration.test.js`
|
||||
- 更新 `openapi.yaml`(加 `GET /api/v1/jobs/{id}/result` path 規格,含 200 / 401 / 404 / 409 / 410 / 500 / 502 / 503 response)
|
||||
- 更新 `README.md`(API 清單加新 endpoint)
|
||||
|
||||
**v0.6 從 v0.5 規劃撤回的 source code 變更**(visionA backend;v0.5 規劃要做、v0.6 不做):
|
||||
|
||||
- **不做**:`visionA-backend/internal/conversion/mc_token_client.go` 部分復活(v0.5 規劃 → v0.6 撤回;commit `86b7175` 已砍除狀態維持)
|
||||
- **不做**:`visionA-backend/internal/conversion/mc_token_client_test.go` 復活
|
||||
- **不做**:`visionA-backend/internal/conversion/faa_client.go` 加 `tokens *MCTokenClient` 欄位 + `DownloadWithDelegated` method(v0.5 規劃 → v0.6 撤回;改成下方「新增 GetResult」)
|
||||
- **不做**:`visionA-backend/internal/conversion/flow.go` 加 `tokens *MCTokenClient` 欄位 + `IssueDelegatedDownload` 步驟(v0.5 規劃 → v0.6 撤回;改成下方「直接呼叫 converter.GetResult」)
|
||||
- **不做**:`OIDCConfig.ServiceClientID` / `ServiceClientSecret` 重新啟用(v0.5 規劃 → v0.6 撤回)
|
||||
- **不做**:`ConversionConfig.TenantID` 重新啟用(v0.5 規劃 → v0.6 撤回)
|
||||
- **不做**:`.env.stage.example` / `.env.dev.example` 加回 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` / `VISIONA_OIDC_TENANT_ID`(v0.5 規劃 → v0.6 撤回)
|
||||
|
||||
**v0.6 visionA backend 實際要做的 source code 變更**:
|
||||
|
||||
- 改 / 改名:`visionA-backend/internal/conversion/faa_client.go` → **改名為 `converter_result_client.go`**(或併入 `converter_client.go` 作為新 method),唯一職責是打 converter `GET /api/v1/jobs/{id}/result` 拉 NEF stream
|
||||
- 改:`visionA-backend/internal/conversion/converter_client.go` —
|
||||
- 維持既有 init / poll / promote method 不變
|
||||
- **新增 method**:`GetResult(ctx context.Context, jobID string) (io.ReadCloser, *DownloadMetadata, error)`,內部 `GET {baseURL}/api/v1/jobs/{jobID}/result` + `Authorization: Bearer <ConverterAPIKey>`,解析 response Content-Length / Content-Disposition / body stream;error mapping 對應 ADR-016 §1.3
|
||||
- 改:`visionA-backend/internal/conversion/flow.go` —
|
||||
- 移除 `tokens *MCTokenClient` 欄位(如尚未從 commit `86b7175` 完全清掉)
|
||||
- 移除 `faa *FAAClient` 欄位(如尚未清掉)
|
||||
- `DownloadStream` 內部:`ensurePromoted` 之後直接呼叫 `flow.converter.GetResult(ctx, jobID)`
|
||||
- `PromoteToModels` 內部:同樣改呼叫 `flow.converter.GetResult(ctx, jobID)`(與 DownloadStream 共用同條 path)
|
||||
- filename 處理:拿到 stream 後用 `defaultDownloadFilename(cj)` 覆寫 converter 給的 filename(不變、規則同 v0.5)
|
||||
- 改:`visionA-backend/internal/conversion/conversion.go` —
|
||||
- `Service.DownloadStream` 簽名不變
|
||||
- 刪除 v0.5 註解中的 `DownloadGrant` struct(不再需要)
|
||||
- 改:`visionA-backend/internal/config/config.go` —
|
||||
- `ConversionConfig`:新增 `ConverterAPIKey` / `FAAAPIKey` 兩欄位
|
||||
- `ConversionConfig.Enabled()` 加入兩個 API key 非空檢查
|
||||
- `OIDCConfig.ServiceClientID` / `ServiceClientSecret`:conversion 不再依賴;如其他模組未使用即從 struct 移除(檢查 grep)
|
||||
- `ConversionConfig.TenantID`:conversion 不再依賴;如其他模組未使用即移除
|
||||
- 改:`visionA-backend/cmd/api-server/main.go` — wire conversion.Flow 時不再傳 MCTokenClient;改傳兩個 API key
|
||||
- 改:`.env.stage.example` / `.env.dev.example` — 移除 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` / `VISIONA_OIDC_TENANT_ID`;新增 `VISIONA_CONVERTER_API_KEY` / `VISIONA_FAA_API_KEY`
|
||||
- 改:對應的 unit test / integration test — 移除 MC mock;改用 fake converter / FAA server,驗 `Authorization: Bearer <apiKey>` header 正確帶上
|
||||
- 不動:`internal/model/*`(schema 不變)
|
||||
- 不動:`internal/api/models.go`(既有 init/finalize 不動,flow.PromoteToModels 內部呼叫 helper)
|
||||
- 不動:OIDC user login 相關全部(`internal/oidc/`、`internal/usersession/`、`/api/auth/*` handlers)
|
||||
- `ConversionConfig.FAAAPIKey` 維持移除(v0.5 撤回過、v0.6 維持)
|
||||
- **新增移除**:`ConversionConfig.FAABaseURL`(v0.5 規劃要保留 / v0.6 移除)
|
||||
- `ConversionConfig.TenantID` 維持移除(v0.5 規劃要加回 / v0.6 撤回)
|
||||
- `OIDCConfig.ServiceClientID` / `ServiceClientSecret` 維持移除(v0.5 規劃要加回 / v0.6 撤回)
|
||||
- `ConversionConfig.Enabled()` 簡化:只判 `ConverterBaseURL != "" && ConverterAPIKey != ""`
|
||||
- 改:`visionA-backend/cmd/api-server/main.go` — wire `conversion.Flow` 時不傳 `MCTokenClient`、不傳 `FAAClient`、不傳 FAA / Tenant config
|
||||
- 改:`.env.stage.example` / `.env.dev.example` —
|
||||
- 維持移除 `VISIONA_FAA_API_KEY`(v0.4 加 / v0.5 撤回 / v0.6 維持撤回)
|
||||
- 維持移除 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` / `VISIONA_OIDC_TENANT_ID`
|
||||
- **新增移除**:`VISIONA_FAA_BASE_URL`(v0.5 規劃要保留 / v0.6 移除)
|
||||
- 保留 `VISIONA_CONVERTER_BASE_URL` / `VISIONA_CONVERTER_API_KEY`
|
||||
- 改:對應 unit / integration test — `converter_client_test.go` 加 `GetResult` test(含 401 / 404 / 410 / 5xx mapping);刪除 `faa_client_test.go`(v0.5 規劃要保留 / v0.6 撤回)
|
||||
|
||||
**v0.6 維持 v0.5 / v0.4 既有的 source code 變更**(converter API key 線不變):
|
||||
|
||||
- 維持:`visionA-backend/internal/conversion/converter_client.go` 仍用 `apiKey string` + `Authorization: Bearer <ConverterAPIKey>`(init / poll / promote method,沒變)
|
||||
- 維持:`Service` interface `DownloadStream(...) (io.ReadCloser, *DownloadMetadata, error)`(v0.4 改的、v0.5 / v0.6 不退回 DownloadRedirectURL)
|
||||
- 維持:`visionA-backend/internal/api/conversion.go` `conversionDownloadHandler` 仍用 `io.CopyN(c.Writer, stream, 1<<30)`(v0.4 改的、v0.5 / v0.6 不退回 302)
|
||||
- 維持:`ConversionConfig.ConverterAPIKey` 欄位 + `Enabled()` 中對 ConverterAPIKey 的檢查
|
||||
|
||||
**不動(v0.4 / v0.5 / v0.6 都不影響)**:
|
||||
|
||||
- `internal/model/*`(schema 不變)
|
||||
- `internal/api/models.go`(既有 init/finalize 不動,flow.PromoteToModels 內部呼叫 helper)
|
||||
- OIDC user login 相關全部(`internal/oidc/`、`internal/usersession/`、`/api/auth/*` handlers)
|
||||
|
||||
> ⚠️ **本次(2026-05-16 / v0.6)的範圍只動共享文件**(本文 conversion.md / api-conversion.md / oidc-tdd.md / adr-014 / adr-015 / adr-016)。source code 改造由 backend agent 下次任務處理(visionA backend 撤回 v0.5 規劃 + 加 converter.GetResult method + 改 flow.go + 改 config / .env;converter 端跨 repo 加新 endpoint 由 jimchen 自行處理)。
|
||||
|
||||
---
|
||||
|
||||
@ -1015,3 +1261,5 @@ frontend 用 `<a href>` 觸發時,若失敗 browser 會把錯誤頁顯示在
|
||||
| 2026-04-30 | 0.3 | Phase 0.8 三方交叉審閱回饋整合:§2.6.1 補「visionA-backend 重啟後 lazy rebuild ownership」(議題 #2,A4 方案);§2.6.2 補 expires_at 來源(議題 #7);§4.3.1 streaming proxy 進度語意明確化(議題 #6,採選項 A:等 converter 201 才回 200);§4.3.2 補 cancel cleanup 鏈與 best-effort cancel converter(議題 #5) |
|
||||
| 2026-05-11 | 0.4 | **Phase 0.8b**:服務間認證從 OAuth `client_credentials` 改為 pre-shared API key(對應 [ADR-015](./adr/adr-015-server-to-server-api-key.md))。主要變更:(1) §1 端對端 sequence 拿掉 MC node;(2) §2 砍 `mc_token_client.go` 整個檔;(3) §3 新增「服務間認證(API key)」章節(原 §5 OAuth 章節整段刪除,章節編號 4→5);(4) §4.1 `/download` handler 從 `c.Redirect(302)` 改 server-side stream proxy(Service interface `DownloadRedirectURL` → `DownloadStream`);(5) §6 錯誤碼 mapping 移除 MC 4 個 code、新增 `converter_auth_failed` / `faa_auth_failed`;(6) §9.1 retry 矩陣移除 MC 2 row、所有下游 401/403 不重試;(7) §10.2 刪除 delegated token TTL、§10.3 改為 pre-shared API key 保護、§10.4 改為 server-side stream proxy 安全模型;(8) 變更影響清單列出 backend agent 後續實作要動的 .go 檔。OIDC user login 完全不動。 |
|
||||
| 2026-05-15 | 0.4.1 | 修 §4.1 `/download` handler `Content-Disposition` filename 來源描述歧義(T4 Reviewer M-3)— 原註釋「filename 來自 promote 結果」可被誤讀為「FAA promote response 直接給 filename」;改為明確標示「visionA backend 在 service 層由 `defaultDownloadFilename(cj)` 從 conversion job metadata 構造(規則 `<source_filename_stem>_<target_chip_lower>.nef`),對齊 wireframe success card 顯示範例」、並補充「FAA 端的 object_key 是 `models/<user>/<job>.nef` 對 user 不友善」的對比說明。純文字釐清、無實作行為變更。 |
|
||||
| 2026-05-16 | 0.5 | **對應 ADR-015 v2.0 範圍縮限**:撤回 v0.4「visionA → FAA 改 API key」決定、FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token);visionA → converter API key 路線(v0.4)**維持**。主要變更:(1) §1 整體 flow sequence 加回 MC node、download path 改回「MC issue delegated token → visionA 帶 delegated token 打 FAA」;(2) §2 模組設計 — mc_token_client.go 部分復活(保留 service token cache + IssueDelegatedDownload 邏輯)、faa_client.go 改 `DownloadWithDelegated(ctx, delegatedToken, objectKey)`、flow.go 加回 `tokens *MCTokenClient` 欄位、DownloadStream / PromoteToModels 流程加回 IssueDelegatedDownload 步驟;(3) §3 拆成 §3.1 visionA → converter(API key)+ §3.2 visionA → FAA(service token + delegated download token),§3.2 詳述 FAA dual-auth 設計與為什麼 download endpoint 強制用 delegated token;(4) §4.1 download handler 流程改回「ensurePromoted → IssueDelegatedDownload → DownloadWithDelegated」(保留 server-side stream proxy 不退回 302);(5) §6 錯誤碼回收 `mc_token_unavailable` / `download_token_failed` 兩個 code,撤回 v0.4 加的 `faa_auth_failed`;(6) §9 retry 矩陣回收 MC 兩 row、FAA row 改回 service token + delegated;(7) §10 安全考量 — §10.2 delegated token TTL 回收、§10.3 API key 保護縮限至 converter、新增 §10.4 MC service token + delegated download token 保護、§10.6 三方對比加 v0.5 column;(8) 變更影響清單列出 backend agent 下次任務範圍(從 v0.4 回退 FAA 線 + mc_token_client 部分復活)。**本次純文件修訂、source code 改造留給 backend agent 下次任務**。OIDC user login 完全不動。 |
|
||||
| 2026-05-16 | 0.6 | **對應 [ADR-016](./adr/adr-016-download-via-converter.md)**:撤回 v0.5「visionA → FAA 線回到 MC service token + delegated download token」**全部規劃**。原因:對 MC source 全 grep 驗證後確認 MC **沒有** `POST /file-access/download-tokens` endpoint、也沒有 FAA `MemberCenterDelegatedDownloadTokenValidator` assume 的 introspection endpoint—— ADR-014 §2 與 ADR-015 v2.0 §2 的 delegated token 鏈是 fictional(從 2026-05-02 寫定起未曾 e2e 跑通)。**v0.6 新設計**:visionA download 改走 **converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉**;visionA 端不再有任何 visionA → MC / visionA → FAA 路徑、server-to-server 認證收斂為單條 visionA → converter(API key);promote 仍走 visionA → converter `POST /promote`(converter 內部 PUT FAA 與 visionA 無關,不變)。主要變更:(1) §1 整體 flow sequence 移除 MC node、download path 改成「converter.GetResult」;(2) §2 模組設計 — mc_token_client.go 維持砍除(撤回 v0.5 部分復活)、faa_client.go 改名為 converter_result_client.go(或併入 converter_client.go),新增 `GetResult` method、flow.go 移除 `tokens *MCTokenClient` 欄位、DownloadStream / PromoteToModels 都改走 converter.GetResult;(3) §3 整段重寫 — §3.1 visionA → converter API key(不變、新增同 key 用於 GetResult endpoint)+ §3.2 visionA → FAA 整段撤回(§3.2.1~§3.2.5 全部標 v0.6 撤回);(4) §4.1 download handler 流程改成「ensurePromoted → converter.GetResult」(保留 server-side stream proxy 不退回 302);(5) §6 錯誤碼撤回 `faa_unavailable` / `mc_token_unavailable` / `download_token_failed` 三個 code、新增 `result_not_found` / `result_expired` 兩個 code;(6) §9 retry 矩陣移除 MC 兩 row、FAA row 全部撤回、新增 Converter GetResult row;(7) §10 安全考量 — §10.2 delegated token TTL 整段撤回、§10.3 API key 保護維持縮限至 converter 同時新增「同一把用於 GetResult」說明、§10.4 MC service token + delegated download token 保護整段撤回、§10.6 加 v0.6 column 對比、§10.7 race condition 與 §10.8 DoS(重編號)更新;(8) 變更影響清單列出 backend agent 下次任務範圍(從 v0.5 規劃撤回 + 新增 converter.GetResult + 跨 repo converter scheduler 加 endpoint)。**本次純文件修訂、source code 改造留給 backend agent 下次任務 + converter 跨 repo 由 jimchen 自行處理**。OIDC user login 完全不動。 |
|
||||
|
||||
@ -2,17 +2,18 @@
|
||||
|
||||
## Metadata
|
||||
- **作者**:Architect Agent
|
||||
- **狀態**:Phase 0.8b 修訂(service client / server-to-server 改 API key;user login 部分不變)
|
||||
- **最後更新**:2026-05-11
|
||||
- **狀態**:Phase 0.8b v0.4 修訂(converter 線改 API key;FAA 線**完全撤回**改走 ADR-016 converter 中轉;user login 部分不變)
|
||||
- **最後更新**:2026-05-16
|
||||
- **文件角色**:Phase 0.6 把 visionA-backend 的 `StaticAuthProvider` 替換為 OIDC 接 Innovedus Member Center
|
||||
- **上位文件**:`TDD.md`、`security.md`、`adr/adr-005-no-db-auth-in-prototype.md`、`adr/adr-010-oidc-bff.md`、`adr/adr-013-public-client.md`、[`adr/adr-015-server-to-server-api-key.md`](./adr/adr-015-server-to-server-api-key.md)(Phase 0.8b 服務間認證部分 supersede ADR-014 §5/§6 service token 段落;user login 部分不受影響)
|
||||
- **上位文件**:`TDD.md`、`security.md`、`adr/adr-005-no-db-auth-in-prototype.md`、`adr/adr-010-oidc-bff.md`、`adr/adr-013-public-client.md`、[`adr/adr-015-server-to-server-api-key.md`](./adr/adr-015-server-to-server-api-key.md) **v2.1**(converter 線改 API key 維持;§2 visionA → FAA 整段被 ADR-016 supersede)、[`adr/adr-016-download-via-converter.md`](./adr/adr-016-download-via-converter.md)(v0.6/v0.4 新增上位:visionA → FAA / MC 路徑完全撤回、改走 converter 中轉)
|
||||
- **下位文件**:`adr/adr-010-oidc-bff.md`(本文件 §16)
|
||||
- **讀者**:Backend / Frontend / DevOps / Testing Agents
|
||||
|
||||
> **Phase 0.8b 範圍說明(重要)**:
|
||||
> **Phase 0.8b v0.4 範圍說明(重要)**:
|
||||
>
|
||||
> - **user login(browser → visionA backend)**:完全不變。仍走 PKCE-only public client、ADR-013 描述的 redirect flow、cookie session、JWKS 驗 id_token,本文件 §1-§12、§14-§17 全部仍有效。
|
||||
> - **server-to-server(visionA backend → converter / FAA)**:Phase 0.8b 改用 pre-shared API key 取代原本的 OAuth `client_credentials` grant。詳見 ADR-015;本文件 §13.1 「Service Client 預留欄位」段落隨之更新(**改為標示廢棄、不再啟用**)。
|
||||
> - **server-to-server(visionA backend → converter)**:v1.0 改用 pre-shared API key 取代 OAuth `client_credentials` grant;v2.0 / v0.4 維持。詳見 ADR-015 §1。
|
||||
> - **server-to-server(visionA backend → FAA / MC)**:v1.x 改 API key 已於 v2.0 撤回;v2.0 規劃回到 ADR-014 §2 原設計(MC service token + delegated download token)— **本文件 v0.4 整段再次撤回**。對 MC source 驗證後確認該 endpoint 從未存在;visionA download 改走 [ADR-016](./adr/adr-016-download-via-converter.md)(converter 新增 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉)。本文件 §13.1 「Service Client」相關欄位(v1.x 標廢棄 → v2.0 規劃啟用)**v0.4 維持廢棄**。
|
||||
|
||||
---
|
||||
|
||||
@ -1455,10 +1456,12 @@ VISIONA_PAIRING_TOKEN=vAc_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
| `VISIONA_OIDC_CLIENT_SECRET` | — | **選填**(A1 改造後)| 為空時走 **public PKCE-only mode**;非空時走 confidential mode(ADR-013)|
|
||||
| `VISIONA_OIDC_REDIRECT_URL` | — | ✅ | dev: `http://localhost:3721/api/auth/callback`;stage: `https://stage-9527.innovedus.com:9527/api/auth/callback`;prod: `https://api.visiona.cloud/api/auth/callback` |
|
||||
| `VISIONA_OIDC_SCOPES` | `openid email profile` | — | 空格分隔 |
|
||||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_ID`~~ | — | **Phase 0.8b 廢棄** | ~~client_credentials grant 用的 confidential client ID~~。Phase 0.8 短暫啟用後,Phase 0.8b 改用 API key(ADR-015)取代;env 從 `.env.stage.example` 移除 |
|
||||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_SECRET`~~ | — | **Phase 0.8b 廢棄** | 同上廢棄;stage 上已洩漏的 secret 直接作廢、不 rotate |
|
||||
| `VISIONA_CONVERTER_API_KEY` | — | **Phase 0.8b 新增** | visionA → converter 服務間認證 pre-shared API key(64 字元 hex),詳見 ADR-015 與 `conversion.md` §3 |
|
||||
| `VISIONA_FAA_API_KEY` | — | **Phase 0.8b 新增** | visionA → FAA 服務間認證 pre-shared API key(64 字元 hex),詳見 ADR-015 與 `conversion.md` §3 |
|
||||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_ID`~~ | — | **Phase 0.8b v0.4 再次廢棄**(v1.x 廢棄;v2.0 規劃啟用;v0.4 / ADR-016 撤回 v2.0 啟用)| ~~client_credentials grant 用的 confidential client ID — FAA 線~~。visionA 端不再有 visionA → MC server-to-server 路徑;download 改走 [ADR-016](./adr/adr-016-download-via-converter.md) converter 中轉 |
|
||||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_SECRET`~~ | — | **Phase 0.8b v0.4 再次廢棄**(同上)| ~~對應 client_credentials grant 的 secret~~。**舊洩漏值** `RciRUyiCkbd60ikkZGkfQ2xV4r02VW3/j0ASKV/DD/E=` 仍作廢;v0.4 後 visionA 端不再持有 service client secret |
|
||||
| ~~`VISIONA_OIDC_TENANT_ID`~~ | — | **Phase 0.8b v0.4 再次廢棄**(同上)| ~~MC service client 註冊時對應的 tenant~~。visionA 端不再需要 tenant_id claim(FAA 端的 tenant 驗由 converter 自己管,與 visionA 無關)|
|
||||
| `VISIONA_CONVERTER_API_KEY` | — | **Phase 0.8b v1.0 新增;v2.0 / v0.4 維持** | visionA → converter 服務間認證 pre-shared API key(64 字元 hex);**v0.4 起也用於 download 路徑**(converter `GET /api/v1/jobs/{id}/result`)。詳見 ADR-015 §1、ADR-016 §1、`conversion.md` §3.1 |
|
||||
| ~~`VISIONA_FAA_API_KEY`~~ | — | **Phase 0.8b v2.0 撤回**(v1.0 加 / v2.0 移除 / v0.4 維持移除)| — |
|
||||
| ~~`VISIONA_FAA_BASE_URL`~~ | — | **Phase 0.8b v0.4 再次廢棄**(既有 / v2.0 維持 / v0.4 移除)| ~~visionA → FAA 的 base URL~~。visionA 端不再直接打 FAA |
|
||||
| `VISIONA_FRONTEND_URL` | — | ✅ | dev: `http://localhost:3000`;stage: `https://stage-9527.innovedus.com:9527`;prod: `https://app.visiona.cloud` |
|
||||
| `VISIONA_SESSION_SECRET` | — | ✅ | 至少 32 byte 隨機字串,HMAC cookie 簽章。產法:`openssl rand -hex 32` |
|
||||
| `VISIONA_SESSION_COOKIE_NAME` | `visiona_session` | — | — |
|
||||
@ -1484,7 +1487,7 @@ VISIONA_SESSION_COOKIE_SECURE=false
|
||||
# Service client 不設(Phase 0.7 不接 MC API)
|
||||
```
|
||||
|
||||
**stage 環境(public PKCE-only client + Phase 0.8b API key 服務間認證)—— Innovedus stage MC 配給的真實 client:**
|
||||
**stage 環境(public PKCE-only client + Phase 0.8b v0.4 單線 server-to-server 認證設計)—— Innovedus stage MC 配給的真實 client:**
|
||||
|
||||
```bash
|
||||
# .env.stage
|
||||
@ -1497,18 +1500,27 @@ VISIONA_FRONTEND_URL=https://stage-9527.innovedus.com:9527
|
||||
VISIONA_SESSION_SECRET=<openssl rand -hex 32 產的值,stage host 持有,不進 git>
|
||||
VISIONA_SESSION_COOKIE_SECURE=true
|
||||
|
||||
# === Phase 0.8b 服務間認證(API key,取代 OAuth service token)===
|
||||
# === Phase 0.8b v0.4 — visionA 端唯一一條 server-to-server 鏈:visionA → converter(API key)===
|
||||
# 同一把 key 用於 init / poll / promote / GET result endpoint
|
||||
VISIONA_CONVERTER_BASE_URL=http://192.168.0.130:9501
|
||||
VISIONA_FAA_BASE_URL=http://192.168.0.130:5081
|
||||
VISIONA_CONVERTER_API_KEY=<openssl rand -hex 32 產的值,與 converter 端 CONVERTER_API_KEY 對齊>
|
||||
VISIONA_FAA_API_KEY=<openssl rand -hex 32 產的值,與 FAA 端 FAA_API_KEY 對齊>
|
||||
|
||||
# === Phase 0.8b 移除(不再使用)===
|
||||
# VISIONA_OIDC_SERVICE_CLIENT_ID=...(已廢棄;ADR-015)
|
||||
# VISIONA_OIDC_SERVICE_CLIENT_SECRET=...(已廢棄;stage 上已洩漏的值直接作廢、不 rotate)
|
||||
# VISIONA_OIDC_TENANT_ID=...(conversion 不再依賴;其他模組未發現使用)
|
||||
# === Phase 0.8b v0.4 撤回(v2.0 規劃要設、v0.4 / ADR-016 撤回)===
|
||||
# visionA 端不再需要 MC service client(visionA → MC server-to-server 路徑完全移除)
|
||||
# VISIONA_OIDC_SERVICE_CLIENT_ID=...
|
||||
# VISIONA_OIDC_SERVICE_CLIENT_SECRET=...
|
||||
# VISIONA_OIDC_TENANT_ID=...
|
||||
# visionA 端不再直接打 FAA(download 走 converter 中轉)
|
||||
# VISIONA_FAA_BASE_URL=...
|
||||
# VISIONA_FAA_API_KEY=...(v1.0 加 / v2.0 撤回 / v0.4 維持撤回)
|
||||
```
|
||||
|
||||
> ⚠️ **secret 處理規則(v0.4)**:
|
||||
>
|
||||
> - visionA 端不再持有任何 MC service client secret(撤回 v2.0 規劃)
|
||||
> - v1.x 在對話中外洩的舊 secret `RciRUyiCkbd60ikkZGkfQ2xV4r02VW3/j0ASKV/DD/E=`(舊 client `23605e14...` 的)仍作廢、不會被 visionA 重新引用
|
||||
> - v2.0 提供的 `4242ba63099d4f318dd3f143d27ef4c5` service client + 對應 secret 在 v0.4 不再被 visionA 使用;若 MC team / 其他產品線需要可獨立持有,與 visionA 無關
|
||||
|
||||
**prod 環境(依 IT 配置):**
|
||||
|
||||
prod MC client 是 public 還是 confidential 由 Innovedus IT 在註冊 OAuth client 時決定。visionA-backend 兩者都支援,env 設 / 不設 `VISIONA_OIDC_CLIENT_SECRET` 即可,**不需重 build**(同一份 binary)。
|
||||
@ -1526,23 +1538,34 @@ api-server 啟動時應 log 一行(**不可 log secret 本身**):
|
||||
判斷依據:`OIDCConfig.ClientSecret == ""`。
|
||||
這條 log 是排查「為什麼 token exchange 401」的第一步 — IdP 註冊的 client 類型必須與 visionA-backend 啟動時的 mode 對齊(兩端錯配會 401 unauthorized client)。
|
||||
|
||||
#### 13.1.3 Phase 0.8b — Server-to-server 認證從 MC OIDC 解耦
|
||||
#### 13.1.3 Phase 0.8b v0.4 — Server-to-server 認證單線設計(visionA 端只剩 visionA → converter)
|
||||
|
||||
> Phase 0.6-0.8 階段保留的「Service Client」概念在 Phase 0.8b 全面廢棄。詳見 [ADR-015](./adr/adr-015-server-to-server-api-key.md) 與 `conversion.md` §3。
|
||||
> v1.x「Phase 0.8b 全面廢棄 Service Client 概念」→ v2.0 部分撤回(FAA 線復活)→ **v0.4 / ADR-016 再次撤回 v2.0**(FAA 線完全移除)。
|
||||
>
|
||||
> 摘要:
|
||||
> 詳見 [ADR-016](./adr/adr-016-download-via-converter.md)、`conversion.md` §3、ADR-015 v2.1。
|
||||
>
|
||||
> - **不再透過 MC**:visionA → converter / FAA 不再走 `POST /oauth/token` 換 service token + JWKS 驗 + scope 驗 的鏈路
|
||||
> - **改用 pre-shared API key**:每個下游各自獨立的 64-hex API key(`VISIONA_CONVERTER_API_KEY` / `VISIONA_FAA_API_KEY`)
|
||||
> - **header 格式不變**:仍是 `Authorization: Bearer <key>`,只是 token 來源從「MC 簽的 JWT」變成「visionA env 內的 pre-shared secret」
|
||||
> - **converter / FAA 端 middleware 同步改寫**:constant-time compare env 字串,不再驗 JWKS / scope / tenant
|
||||
> **v0.4 摘要**(visionA 端只剩一條 server-to-server 線):
|
||||
>
|
||||
> **為什麼把這個段落放在 OIDC TDD**:原本 ADR-014 §5 把「service client / client_credentials grant」與「user login OIDC」放同一條 OIDC 整合線;Phase 0.8b 後這兩條線完全脫鉤:
|
||||
> **visionA → converter(與 user login 解耦)**:
|
||||
> - **不透過 MC**:不走 `POST /oauth/token` 換 service token + JWKS 驗 + scope 驗的鏈路
|
||||
> - **改用 pre-shared API key**:`VISIONA_CONVERTER_API_KEY`(64-hex)
|
||||
> - **header 格式**:`Authorization: Bearer <key>`
|
||||
> - **converter 端 middleware**:constant-time compare env 字串,不驗 JWKS / scope / tenant
|
||||
> - **endpoints**:`POST /api/v1/jobs`(init)、`GET /api/v1/jobs/:id`(poll)、`POST /api/v1/jobs/:id/promote`(promote)、**`GET /api/v1/jobs/:id/result`(v0.4 新增;download)**
|
||||
>
|
||||
> - user login:仍是 OIDC(PKCE / cookie session / JWKS) — 本文件 §1-§12 全部適用
|
||||
> - server-to-server:不再是 OIDC、不再屬於本文件範圍 — 看 `conversion.md` §3 與 ADR-015
|
||||
> **visionA → FAA / MC(v0.4 整段撤回)**:
|
||||
> - v1.x 改 API key 已撤回(v2.0);v2.0 規劃要回到 MC service token + delegated download token 路徑 → **v0.4 整段再次撤回**
|
||||
> - **原因(致命發現 2026-05-16)**:對 MC source 全 grep 驗證後確認 MC 沒有 `POST /file-access/download-tokens` endpoint、也沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint—— v2.0 規劃的路徑是 fictional、從未實際 e2e 跑通
|
||||
> - **v0.4 處理**:visionA 端不再有任何 visionA → MC / visionA → FAA server-to-server 路徑;download 改走 converter 中轉(converter 自己用 OAuth client_credentials + `files:upload.write` scope 推 FAA、後續 download 從 converter MinIO stream 回 visionA,converter → FAA 鏈路 Phase 1 已上線、與 visionA 無關)
|
||||
> - **MC 不再是 visionA 的 server-to-server 依賴**;MC 仍是 visionA user login 的 OIDC IdP(公開 PKCE client、本文件 §1-§12),但兩條線完全獨立
|
||||
>
|
||||
> 此小節保留作為「OIDC 路徑 → API key 路徑」的指引,避免讀者讀到本文件 §13.1 看到舊 service client env 還以為要啟用。
|
||||
> **為什麼 v0.4 撤回 v2.0**:v2.0 規劃時 architect agent 沒實際讀 MC source、沿用 ADR-014 §2 的 assume;對 source 驗證後發現 ADR-014 §2 從 2026-05-02 寫定起就是 broken design(MC 沒有對應 endpoint),整條鏈從未 e2e 跑通。v0.4 改採 ADR-016(converter 加新 endpoint),visionA + converter 都是 jimchen 自己維護的 repo、可單方控制,不必動 MC / FAA / warrenchen。
|
||||
>
|
||||
> - **user login**:仍是 OIDC(PKCE / cookie session / JWKS)— 本文件 §1-§12 全部適用
|
||||
> - **server-to-server visionA → converter**:不是 OIDC、不屬於本文件範圍 — 看 `conversion.md` §3.1 與 ADR-015 §1
|
||||
> - **server-to-server visionA → FAA**:**不存在**(v0.4 撤回 v2.0 規劃;不再使用本文件 §13.1 列出的 OIDC ServiceClient* / TenantID 三個 env)
|
||||
>
|
||||
> **本小節保留的用途(v0.4 修訂)**:說明 v1.x 廢棄 → v2.0 復活 → v0.4 再次廢棄的完整時序,並標清「OIDC ServiceClient* 三個 env 在 v0.4 後維持廢棄」,避免讀者誤啟用。
|
||||
|
||||
### 13.2 visionA-frontend 新增
|
||||
|
||||
@ -1845,3 +1868,5 @@ OF1 (與 OF2 平行)
|
||||
|------|------|------|
|
||||
| 2026-04-26 | 0.1 | Architect Agent 初稿(Phase 0.6 OIDC 接入 TDD 增補) |
|
||||
| 2026-05-11 | 0.2 | **Phase 0.8b** 對應 [ADR-015](./adr/adr-015-server-to-server-api-key.md):(1) Metadata 區補 Phase 0.8b 範圍說明(user login 不變、server-to-server 改 API key);(2) §13.1 環境變數表把 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` 兩 row 標廢棄、新增 `VISIONA_CONVERTER_API_KEY` / `VISIONA_FAA_API_KEY` 兩 row;(3) §13.1.1 stage env 範例移除 service client 區、新增 API key 區;(4) 新增 §13.1.3 說明 server-to-server 與 user login OIDC 已脫鉤,引導讀者去 `conversion.md` §3 與 ADR-015。本文件其他章節(§1-§12、§14-§17)關於 user login 部分全部不變。 |
|
||||
| 2026-05-16 | 0.3 | **對應 ADR-015 v2.0 範圍縮限**:撤回 v0.2 「server-to-server 全部改 API key、service client env 全部廢棄」決定。FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token),需要 MC OIDC service client;converter 線維持 v0.2 的 API key 路線。主要變更:(1) Metadata 區更新 v2.0 範圍說明 — 區分 converter 線(API key)與 FAA 線(仍 OAuth);(2) §13.1 環境變數表 — `VISIONA_OIDC_SERVICE_CLIENT_ID` / `VISIONA_OIDC_SERVICE_CLIENT_SECRET` / `VISIONA_OIDC_TENANT_ID` 三 row 從廢棄改回「v2.0 重新啟用」、補 stage 對應值(`4242ba63...` / `732270c0-...`),`VISIONA_FAA_API_KEY` row 標撤回,`VISIONA_FAA_BASE_URL` row 補 stage 值;(3) §13.1.1 stage env 範例加回 service client + tenant_id(用 placeholder)、保留 converter API key、撤回 `VISIONA_FAA_API_KEY`、補 secret 處理規則注意事項;(4) §13.1.3 改為「v2.0 雙線設計」說明 — converter 線解耦 / FAA 線未解耦、引導讀者看 `conversion.md` §3.1 vs §3.2 / ADR-015 v2.0 §1 vs §2 / ADR-014 §2。本文件 user login 章節(§1-§12、§14-§17)全部不變。 |
|
||||
| 2026-05-16 | 0.4 | **對應 [ADR-016](./adr/adr-016-download-via-converter.md)**:再次撤回 v0.3「server-to-server FAA 線回到 MC service token + delegated download token」全部規劃。原因:對 MC source 全 grep 驗證後確認 MC **沒有** `POST /file-access/download-tokens` endpoint、也沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint—— ADR-014 §2 / ADR-015 v2.0 §2 的 delegated token 鏈從 2026-05-02 起即為 broken design、從未 e2e 跑通。**v0.4 新設計**:visionA download 改走 converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉;visionA 端 server-to-server 鏈路收斂為單條 visionA → converter(API key)。主要變更:(1) Metadata 區更新 v0.4 範圍說明 — visionA → FAA / MC 整段撤回;(2) 上位文件改為 ADR-015 v2.1 + ADR-016;(3) §13.1 環境變數表 — `VISIONA_OIDC_SERVICE_CLIENT_ID` / `_SECRET` / `VISIONA_OIDC_TENANT_ID` 三 row 從 v0.3「重新啟用」改回「再次廢棄」、`VISIONA_FAA_BASE_URL` row 從「使用中」改為「再次廢棄」、`VISIONA_CONVERTER_API_KEY` row 補「v0.4 起也用於 download 路徑」說明;(4) §13.1.1 stage env 範例移除 service client + tenant_id + FAA URL(v0.3 加回的全部撤回)、加註「visionA 端不再持有 MC service client secret」、secret 處理規則整段改寫;(5) §13.1.3 改寫為「v0.4 單線設計」說明 — visionA → FAA / MC 全部撤回,加完整時序(v1.x 廢棄 → v2.0 復活 → v0.4 再次廢棄)、引導讀者看 conversion.md §3 / ADR-016 / ADR-015 v2.1;(6) 加完整撤回理由與「為什麼 v0.4 撤回 v2.0」說明。本文件 user login 章節(§1-§12、§14-§17)全部不變、Phase 0.6 OIDC 接 MC 的設計完全不受影響。 |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user