模型庫下載原本前端用 fetch + Authorization: Bearer 跨 origin 直連 FAA,
觸發 CORS preflight(OPTIONS);FAA 未設 CORS、OPTIONS 回 405 → 下載失敗。
FAA 設計本就支援「token 放 query string(access_token) + redirect 導航下載」,
故不需 FAA 設 CORS。改採 ADR-017 §11 v1.3 乙案:
- backend: download_url 組成含 ?access_token={url.QueryEscape(token)} 完整 FAA URL;
ModelDownloadResponse.Token 標 deprecated 保留(向下相容);token 不進 log
- frontend: 移除 downloadModelFile/deriveDownloadFilename(fetch+blob),
改 triggerNavDownload 用 <a download href> 導航;ModelDownloadGrant 移除 token 欄
- 整條鏈無 CORS:前端→backend 同 origin、前端→FAA 導航無 preflight
docs: ADR-017 增補 §11(v1.3) + TDD.md §9.5
tests: backend 8 PASS(含 token escape 邊界)、frontend 35 PASS;Reviewer 通過(0C/0M)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
642 lines
68 KiB
Markdown
642 lines
68 KiB
Markdown
# ADR-017: 模型庫存取架構(File Access Agent 重設計)
|
||
|
||
## 狀態
|
||
Proposed(待使用者裁決)— **v1.3 修訂(2026-06-27):瀏覽器下載改 query-string token + redirect(推翻 v1.2 決策 2 / §10.4 的「fetch + Authorization Bearer」),徹底解 CORS 405。詳見 §11。** **v1.2 修訂(2026-06-07):(a) 已用 stage 真實環境 + 真 secret + 真 user e2e 實測打通,跨團隊 blocking 歸零,剩餘全是 visionA 端純實作。** 保留 v1.0(推 (c))/ v1.1(改推 (a)、待跨團隊驗證)歷史。
|
||
|
||
## 日期
|
||
2026-06-06(v1.0)/ 2026-06-06(v1.1 修訂)/ 2026-06-07(v1.2 修訂)
|
||
|
||
## 作者
|
||
Architect Agent
|
||
|
||
---
|
||
|
||
## 0. v1.1 修訂註記(重要 — 推翻 v1.0 前提)
|
||
|
||
> **v1.0(同日稍早)推薦決策 1 = (c) visionA 自簽 token**,核心理由是「(a) 回到 MC token 那條鏈一年沒生出來、是 fictional(見 ADR-016 致命發現)」。
|
||
>
|
||
> **這個前提在 v1.1 被推翻。** 重新驗證 source 後發現:上週(ADR-016)判 (a) fictional 時**只 grep 了 MC master 分支**,沒看 develop。實際上:
|
||
>
|
||
> 1. **MC `develop` 分支已實作** `POST /file-access/download-tokens`(Issue)+ `POST /file-access/download-tokens/validate`(Validate)兩個 endpoint(commit `e77fdec`),master 尚未 merge。
|
||
> 2. **FAA 端 `MemberCenterDelegatedDownloadTokenValidator.cs` 本來就是配套**:它打 MC validate 的 payload(`token/tenant_id/file_id/object_key/method`)與 response(`active/...`)形狀,**與 MC develop 的 validate endpoint 契約吻合**(見本 ADR §9 驗證證據)。
|
||
> 3. 所以 ADR-014 那條 `visionA → MC → FAA` delegated token 鏈,在 **source level 是通的**,缺的只是「部署 + scope 註冊 + 跨 repo 設定」,不是「程式碼不存在」。
|
||
>
|
||
> **使用者原本就想 follow pptx 設計(走 MC token = (a))。** 既然 (a) 不再是「賭 MC 一年生不出來的 endpoint」、而是「已寫好、待部署」,本 ADR v1.1 **改推 (a)**,並把 (c) 降為「跨團隊部署若卡住的 fallback」。
|
||
>
|
||
> **但 v1.1 必須誠實標記**:source 就緒 ≠ 零風險。(a) 仍有**跨團隊部署依賴**(warrenchen 要 merge develop→master + 部署 MC stage + 設 FAA stage MemberCenterOptions + MC 端註冊 visionA service client 的 `files:download.read/delegate` scope)。這條鏈過去 5/9 就因「scope 沒在 MC 註冊」撞 `invalid_scope`——同類風險仍在。詳見 §4 決策 1(改寫)與 §9(可行性驗證)。
|
||
>
|
||
> **被 supersede 的內容**:v1.0 §4 決策 1 的「推薦 (c)」結論、決策 2 的「visionA 自簽 JWT」具體流程、§5 落地順序 P0、§6 Q1、§7 R1。原文保留於下方並標 ~~刪除線~~ / 修訂框,保留歷史脈絡。
|
||
|
||
---
|
||
|
||
## 0.1 v1.2 修訂註記(重要 — (a) 已 stage 真實環境 e2e 實證打通)
|
||
|
||
> **v1.1 時 (a) 還是「source 就緒、但跨團隊部署 + scope 註冊是未知風險」**(P0 標「warrenchen 主導、不可控」、Q1.5 標「待跟 warrenchen 確認」、R1 標「5/9 撞過 invalid_scope 的同類最高風險」)。
|
||
>
|
||
> **v1.2 的事實:MC stage 已部署、scope 機制已驗證、整條 (a) e2e 認證鏈用真 secret + 真 user_id 跑通了(HTTP 200 簽出 `fdt_` token、FAA 只因測試用假 object_key 回 404 file_not_found,即認證鏈全綠)。跨團隊 blocking 歸零,剩餘全是 visionA 端純實作。** 完整證據見**新增的 §10((a) 端到端 stage 實測證據)**。
|
||
>
|
||
> **v1.1 → v1.2 的核心轉變**:
|
||
> 1. **MC 部署確認**:stage `POST /file-access/download-tokens` 與 `/validate` 回 401(非 404)→ endpoint **已部署**(推翻 v1.0/v1.1「只看 git master 判 fictional」,stage 實際版本已含 develop 功能)。
|
||
> 2. **scope 機制已驗證**:MC 用 usage→scope 模型(`file_api` usage 自動帶 5 個 files scope),FAA 既有 service client `4242ba63...` 實測能拿 `files:download.delegate` token。R1 的「scope 沒在 MC 註冊」風險**已實證可繞過**(短期共用 FAA client、正式上線換 visionA 專屬)。
|
||
> 3. **🏆 e2e 全綠**:使用者提供真實 OIDC user_id,整條 Issue→簽 fdt→FAA validate 鏈 stage 實測通過(B3 tenant 一致、B4 真實 user、FAA validate 鏈全部實證)。
|
||
>
|
||
> **被 v1.2 supersede 的內容**:v1.1 §4 決策 1 的「(a) 待跨團隊部署驗證」、§5 P0「跨團隊部署鏈(warrenchen 主導不可控)」、§6 Q1/Q1.5/Q9「待確認」狀態、§7 R1「最高風險」定性。原文保留並標 v1.2 更新框,保留歷史脈絡。
|
||
>
|
||
> **剩餘 visionA 端 blocking(v1.2 標清楚、為 backend agent 接下來的工作)**:B1 object_key 斷層(最關鍵,第一階段 (a) 只支援轉檔→promote 類 model)+ B2 visionA 加「打 MC /oauth/token + 打 MC Issue 簽 fdt」的 client code + .env 設定。詳見 §10.4。
|
||
|
||
---
|
||
|
||
## 1. 背景與範圍
|
||
|
||
visionA Cloud 的「模型庫」要處理的是**大檔案**(轉檔/訓練好的模型、APP 套件),這類資產的存取設計有兩個本質問題綁在一起:
|
||
|
||
1. **流量費用** — S3 容量便宜但流量貴,大檔反覆下載不該每次都經過 AWS egress。
|
||
2. **權限保護** — Bucket 位址不能曝露;就算有人拿到連結也不能直接下載,每次存取都必須先做身份/權限查驗。
|
||
|
||
本 ADR 收斂三個面向,並把它們放進使用者目標架構(`模型庫存取架構.pptx`,引入 File Access Agent + NAS Bucket)的框架下一起決策,而不是各自打 patch:
|
||
|
||
- **B 模型存取權限/分享**:現況 owner-only → 目標 share/workspace/role。
|
||
- **C 模型下載/檔案存取**:現況**沒有 model download endpoint**,要設計 pptx 的「Client 直連 FAA 下載」。
|
||
- **D promote-to-models 修補**:stage 環境 promote 按鈕卡 OAuth 401,要收斂進新架構而非孤立修補。
|
||
|
||
### 1.1 pptx 目標 vs 現況的根本衝突(本 ADR 存在的理由)
|
||
|
||
pptx 的上傳/下載流程都假設一條 token 鏈:**「visionA 向 MC 取 Token → 帶 Token 給 FAA → FAA 向 MC 驗 Token」**。
|
||
|
||
~~但歷史盤點(見 §2)顯示:MC 沒有簽發 delegated download token 的 endpoint,也沒有 FAA 驗 token 所需的 introspection endpoint。ADR-014 §2 描述的這條鏈從 2026-05-02 起就是 fictional、從未 e2e 通過,上週 ADR-016 才改走「converter 中轉」繞開。~~
|
||
|
||
> **v1.1 修正**:上述「MC 沒有 endpoint」的判斷**只成立於 MC master 分支**。MC `develop` 分支(commit `e77fdec`)**已實作** Issue + Validate 兩個 endpoint,FAA 端 validator 本來就是配套設計。所以這條鏈在 source level 是通的、只差部署。詳見 §0 修訂註記與 §9 可行性驗證。**本 ADR 改在「使用者想 follow pptx(走 MC token)」+「MC develop 已就緒、待部署」的新事實下,決定推動 (a) 並列出跨團隊部署 checklist。**
|
||
|
||
---
|
||
|
||
## 2. 現況盤點
|
||
|
||
| 面向 | 現況事實 | Source |
|
||
|------|---------|--------|
|
||
| **A. FAA 端能力** | `GET /files/{key}`(下載)**沒掛 RequireAuthorization()**,改用 `IDelegatedDownloadTokenValidator.ValidateAsync()` 驗 delegated download token。`PUT`/`GET metadata`/`HEAD`/`DELETE` 走 JWT Bearer + `EnsureJwtScopeAndTenant`(scope: files:upload.write / files:metadata.read / files:delete)。即 FAA 是 **dual-auth**:download=delegated token、其他 op=service JWT。 | `file_access_agent/.../Program.cs:184-254` |
|
||
| **A'. FAA download 原生支援** | FAA download 本來就是為「帶 delegated token 直接下載」設計 → pptx 的「Client 直連 FAA 下載」**FAA 端原生支援**,FAA 不是阻礙。 | 同上 |
|
||
| **B-新事實. MC develop 已有 token 簽發** | MC `develop`(commit `e77fdec`)**已實作** `POST /file-access/download-tokens`(Issue, `[Authorize(Policy="FilesDownloadDelegate")]`,回 opaque `fdt_<base64url>` token + scope `files:download.read` + 自訂 TTL)。**master 尚未 merge**——上週只 grep master 才誤判不存在。 | MC develop `FileAccessController.cs`(Orchestrator 已驗)|
|
||
| **B-新事實'. MC develop 已有 token 驗證** | MC develop **已實作** `POST /file-access/download-tokens/validate`(Validate, `[Authorize(Policy="FilesDownloadRead")]`,驗 TokenHash + RevokedAt + ExpiresAt + boundary(tenant/file/object_key/method),回 `{active, scope, expires_at}`)。FAA validator 的 payload/response 形狀**與此契約吻合**。 | MC develop / FAA validator(本 ADR §9)|
|
||
| **B-結論修正. token 鏈 source 已就緒、待部署** | ADR-014 §2 的「visionA→MC→FAA delegated token 鏈」在 **source level 是通的**(MC develop + FAA validator 配套)。缺的是:MC merge develop→master + 部署 stage + 註冊 visionA service client 的 download scope + FAA stage `MemberCenterOptions` 設定。**不再是 fictional,而是「跨團隊部署待辦」。** | MC develop / FAA / ADR-014 |
|
||
| **C. promote(converter→FAA)prod work** | converter promote → FAA PUT NEF 走 **OAuth client_credentials + scope `files:upload.write`**,**prod 已上線可用**。 | `apps/task-scheduler/.../fileAccessAgent/client.js` |
|
||
| **C'. promote stage 卡 401** | stage 上 promote 按鈕卡 OAuth 401 → 研判是 stage converter 的 OAuth client 設定 / FAA accepted scope 問題(**設定問題,非設計問題**)。 | progress.md |
|
||
| **C''. NEF 雙存** | converter MinIO 存 NEF,promote 後 NEF 同時在 converter MinIO(expires_at=7天)+ FAA。 | ADR-016 |
|
||
| **D. visionA download 現況** | visionA → converter(init/poll/promote/result download)**全走 API key**(ADR-015 §1)。visionA backend 有 stream proxy 結構(`flow.go` DownloadStream:io.CopyN size cap + Content-Disposition + context cancellation),現況 stream 來源是 converter `/result`。 | ADR-015 / `internal/conversion/flow.go` |
|
||
| **D'. 無 model download endpoint** | `internal/api/models.go`:/api/models/* 有 list/get/init/finalize/delete;**load-to-device 是 stub;無 model download endpoint**。 | `internal/api/models.go` |
|
||
| **E. 資料模型現況** | DB `models` table 是 `owner_user_id UUID NOT NULL`(單一擁有者)+ `CREATE INDEX ON models (owner_user_id) WHERE deleted_at IS NULL`。`model.go` Model struct 只有 OwnerUserID,Repository.List 用 OwnerUserID 過濾,**無 share/workspace/role/ACL**。 | `internal/model/model.go` / `database.md:344-363` |
|
||
|
||
**現況一句話總結**:FAA 設計上 ready(download 收 delegated token、op 收 service JWT),converter→FAA 上傳 prod work;但「誰簽發/驗證 download token」這一環,MC 從未提供,visionA 也還沒自建,所以模型下載至今走不通、model download endpoint 根本還沒長出來。
|
||
|
||
---
|
||
|
||
## 3. 目標架構(pptx)解讀
|
||
|
||
### 3.1 核心設計
|
||
|
||
引入 **File Access Agent (FAA) + NAS Bucket(搭配 AWS S3)**。大檔存取統一收口到 FAA:
|
||
|
||
- Bucket 位址不曝露(Client 只跟 FAA 互動,不知道也碰不到底層 Bucket)。
|
||
- 每次存取都過 FAA 的權限查驗。
|
||
- 未來可在 FAA 層加密。
|
||
- **省流量**:除了「上傳那一次」大檔流量不經 AWS;下載走 FAA(NAS/公司網路),不吃 AWS egress。
|
||
- 轉檔/訓練好的模型可以**只傳網址**讓 FAA 自己去抓(converter→FAA 已經是這個模式的雛形)。
|
||
|
||
### 3.2 pptx 上傳流程
|
||
|
||
1. POST visionA endpoint
|
||
2. visionA 向 MC 取 Token
|
||
3. 檔案(連結) + Token 送 FAA
|
||
4. FAA 向 MC 驗 Token
|
||
5. FAA 回檔案 path 給 visionA
|
||
|
||
### 3.3 pptx 下載流程
|
||
|
||
1. visionA 驗存取權限
|
||
2. visionA 向 MC 取 Token
|
||
3. **Client 帶 Token 直接向 FAA 發 GET**
|
||
4. FAA 向 MC 驗 Token
|
||
5. Response 檔案到 Client(**不經 visionA、不經 AWS**)
|
||
|
||
### 3.4 pptx 灰色地帶(必須在 §4 解掉)
|
||
|
||
| # | 灰色地帶 | 問題 |
|
||
|---|---------|------|
|
||
| G1 | **「visionA 向 MC 取 Token」** | ~~MC 沒有簽發 download token 的 endpoint~~ **v1.1:MC develop 已有 Issue endpoint(事實 B-新事實)**。可做到,但 visionA 需「復活 MC service token client」+ MC 端註冊 download scope(見 §9 缺口)。 |
|
||
| G2 | **「FAA 向 MC 驗 Token」** | ~~MC 沒有 introspection endpoint~~ **v1.1:MC develop 已有 Validate endpoint、FAA validator 本來就是配套(事實 B-新事實')**。可做到,但需 FAA stage 設好 `MemberCenterOptions`(見 §9 缺口)。 |
|
||
| G3 | **Client 直連 FAA 的憑證怎麼到 Client 手上** | pptx 說「Client 帶 Token」,但沒講 token 範圍(單檔/全庫)、有效期、防盜用(被截走能不能重放)。 |
|
||
| G4 | **Bucket 不曝露但 FAA 對外** | FAA 要對 Client(含桌面 local-tool / 瀏覽器)開放,CORS、FAA 對外網段、HA 都得想。 |
|
||
| G5 | **pptx 自列待解** | ①HA(NAS/公司網路掛掉就全掛)②頻寬(下載走公司網路,先 router 限流,未來移轉便宜雲端空間)。 |
|
||
|
||
**G1 + G2 是本 ADR 決策 1/決策 2 的核心**:使用者已拍板「Client 直連 FAA」+「要驗權限」,但簽發/驗證 token 的 MC 能力不存在。所以必須決定 **delegated token 由誰簽、由誰驗**。
|
||
|
||
---
|
||
|
||
## 4. 關鍵決策點
|
||
|
||
### 決策 1:認證方向(download token 誰簽發、誰驗證)— **v1.2 改寫((a) 已 stage e2e 實證)**
|
||
|
||
> **v1.2 重大更新**:(a) 已用 stage 真實環境 + 真 secret + 真 user_id **e2e 實測打通**(HTTP 200 簽出 `fdt_` token、FAA validate 鏈全綠,僅因測試用假 object_key 回 404 file_not_found;見新增 §10)。**(a) 從 v1.1 的「已寫好、待跨團隊部署驗證」升級為「已實證可行、跨團隊 blocking 歸零」。** 剩餘全是 visionA 端純實作(object_key 斷層 + 打 MC Issue 的 client code,見 §10.4)。**(c) fallback 實測後已無需動用**——除非未來改用正式專屬 file_api client 時受阻,否則 (a) 直接落地。
|
||
>
|
||
> **v1.1 原文(保留供對照)**:v1.0 在「(a) 是 fictional」的錯誤前提下推 (c)。新事實(MC develop 已實作 Issue+Validate、FAA validator 配套,見 §0/§9)下,(a) 從「賭一年沒生出來的 endpoint」變成「已寫好、待跨團隊部署」。**使用者想 follow pptx(= (a))**,故 v1.1 改推 (a),(c) 降為 fallback。
|
||
|
||
pptx 預設 MC 簽發 + MC 驗證。以下三案(v1.1 重新評估):
|
||
|
||
| 維度 | (a) MC delegated token(pptx 原圖)★v1.1 改推 | (b) 延續 API key / pre-shared secret | (c) visionA 簽短期 token + FAA 本地驗(v1.0 原推,v1.1 降 fallback) |
|
||
|------|---------------------------------------|------------------------------------|--------------------------------------------------------|
|
||
| **做法** | visionA 打 MC `POST /file-access/download-tokens` 拿 opaque `fdt_` token → Client 帶 token 打 FAA `GET /files/{key}` → FAA `IDelegatedDownloadTokenValidator` 打 MC `validate` 驗 | Client 帶固定 pre-shared secret 直連 FAA;FAA 比對 secret | visionA 自簽短期 JWT(含 file key + exp + 權限 claim),FAA 用 visionA JWKS 本地驗簽 |
|
||
| **source 現況(v1.1 新增)** | **MC develop + FAA validator 都已寫好**(commit `e77fdec`),缺部署/scope 註冊/FAA stage 設定 | 無現成 | **FAA validator 目前 impl 是「打 MC validate」、不是「本地驗 visionA JWKS」** — 要 (c) 反而得叫 warrenchen **改寫 FAA validator**(比 (a) 重!)|
|
||
| **與 pptx 相容** | 完全相容(就是 pptx 原圖、使用者想要的) | 半相容 | 相容(token 由 visionA 取代 MC 角色) |
|
||
| **對 MC / FAA team 依賴** | **中** — 不需「新寫」endpoint(已寫好),但需 warrenchen merge develop→master + 部署 MC stage + 設 FAA stage `MemberCenterOptions` + MC 註冊 visionA service client 的 download scope | 無 | **中-高(v1.1 重估)** — FAA 現成 validator 是打 MC 的;改成本地驗 visionA JWKS 要 warrenchen **改 validator impl**,這也是動 FAA、不比 (a) 輕 |
|
||
| **與現況落差** | 中 — visionA 端要**復活 MC service token client**(ADR-016 剛砍掉 mc_token_client.go,要逆轉)+ 新增「打 MC Issue」邏輯 | 中 | 中 — visionA 要建簽發+JWKS;FAA 要改 validator |
|
||
| **防盜用(被截走)** | 佳 — MC 簽 opaque token + 短 TTL(可自訂 `expires_in_seconds`)+ boundary(tenant/file/object_key/method) + 可 revoke(RevokedAt)| **差** — secret 外洩=全庫淪陷 | 佳 — 短 exp + 綁 file key + 綁 user |
|
||
| **HA / 延遲** | 每次下載 FAA→MC 同步 validate,多一跳 + MC 成驗證單點(但 FAA validator 有 service token cache,token validate 是輕量呼叫)| 無外部依賴 | 無外部依賴,FAA 本地驗簽,延遲最低 |
|
||
| **成本/工時** | 中 — visionA 復活 MC client(逆轉 ADR-016)+ 跨團隊部署協調(工期受 warrenchen 排程影響) | 低(但留安全債) | 中 — visionA 簽發+JWKS + **FAA validator 改寫(跨團隊)** |
|
||
| **opaque token 限制(v1.1 新增)** | token 是 `fdt_<random>` 不是 JWT → FAA **無法本地驗簽**,每次必 call MC validate(這是 MC 設計,符合 FAA 現成 validator)| — | JWT 可本地驗(但 FAA 現成 validator 不是這樣做的)|
|
||
|
||
**v1.2 推薦:(a) MC delegated token(pptx 原圖)— 已 stage e2e 實證可行。** (c) 仍保留為 **fallback**,但實測後已無需動用(除非未來改正式專屬 client 受阻)。
|
||
|
||
**改推 (a) 的理由**:
|
||
1. **(a) 不再是 fictional** — MC develop + FAA validator 是配套的、已寫好(§9 驗證)。v1.0 推 (c) 的唯一核心理由(「MC 那條鏈一年沒生出來」)已不成立。
|
||
2. **(c) 在新事實下反而更重** — FAA 現成的 `MemberCenterDelegatedDownloadTokenValidator` 是「打 MC validate」的 impl。要走 (c) 必須請 warrenchen **改寫 validator 成本地驗 visionA JWKS**——這同樣是動 FAA、且是「改既有正確配套」,比「部署既有 (a)」更不划算。
|
||
3. **符合使用者意圖** — 使用者原本就想 follow pptx 設計(走 MC token)。(a) = pptx 原圖。
|
||
4. **FAA / MC 端零程式碼改動** — (a) 在 source level 已就緒,FAA / MC 不需寫新 code,只需「部署 + 設定 + scope 註冊」。
|
||
5. **token 安全性最強** — MC 簽 opaque token,支援 boundary 檢查 + revoke + 自訂 TTL,比 visionA 自簽 JWT(無法 revoke)更完整。
|
||
|
||
> **(a) 的關鍵前置(跨團隊部署 checklist,非程式碼)**:
|
||
> 1. **[MC / warrenchen]** merge `develop`(含 `e77fdec`+`5f32452`)→ `master` + 部署 MC stage。
|
||
> 2. **[MC / warrenchen]** 在 MC 端把 visionA service client(`23605e14...`)的 `files:download.read` + `files:download.delegate` scope **註冊到 AuthResourceRegistry**(這是 5/9 撞 `invalid_scope` 的同類風險點——ADR-014 line 35 雖列了 4 scope,但「列在 visionA 文件」≠「在 MC 端註冊」)。
|
||
> 3. **[FAA / warrenchen]** FAA stage 部署 + 設好 `MemberCenterOptions`(`BaseUrl` / `ClientId` / `ClientSecret` / `Scope` / `DownloadTokenValidationPath` 指向 MC develop 的 `/file-access/download-tokens/validate`)。
|
||
> 4. **[visionA / jimchen]** **復活 MC service token client**(逆轉 ADR-016 §5 砍除的 mc_token_client.go)+ 新增「打 MC Issue endpoint 拿 fdt_ token」邏輯。
|
||
> 5. **[三方]** 對齊 boundary:visionA Issue 時帶的 `object_key` 必須 = FAA `GET /files/{key}` 的 key = MC validate 的 `object_key`(見決策 2 與 §9 的 object_key 斷層分析)。
|
||
>
|
||
> **✅ v1.2 更新:此前置清單 step 1–3、5 已 stage 實測驗證完成。** MC stage 已部署(step 1,§10.1);scope 機制已驗證——FAA service client `4242ba63...` 實測能拿 `files:download.delegate` token、boundary(tenant/object_key/method)e2e 通過(step 2/5,§10.2–§10.3);FAA stage 已部署且 `MemberCenterOptions` 已設好(step 3,§10.3 FAA validate 鏈全綠)。**原「step 2 是最高風險、5/9 撞 invalid_scope」已實證可繞過**(短期共用 FAA client 拿 token,正式上線換 visionA 專屬 usage=file_api client,見 §7 R1 改寫)。**剩 step 4(visionA 復活 MC client + 打 Issue)= 唯一剩餘工作、純 visionA 端、自己可控。**
|
||
|
||
> **~~何時 fallback 回 (c)~~(v1.2:實測後已無需動用)**:v1.1 設想「若跨團隊部署卡住則 fallback (c)」。v1.2 跨團隊部署已 e2e 通、無卡點,**(c) 不再需要動用**。僅保留為「未來 visionA 改用正式專屬 file_api client、若 MC 端無法配發專屬 client 時」的理論退路。**(b) pre-shared secret 仍不採用**。
|
||
|
||
#### v1.0 原推薦((c))保留供對照
|
||
|
||
> ~~**推薦:(c) visionA 簽短期 download token + FAA 本地驗。** 理由:(1) 解除對 MC 的歷史依賴——MC 那兩個 endpoint 一年沒生出來;(2) FAA 本來就是 dual-auth、有 validator 抽象,換 impl 即可;(3) 滿足 pptx 兩大訴求;(4) 未來不排斥 (a)。~~
|
||
>
|
||
> v1.1 supersede 原因:理由 (1) 的前提「MC endpoint 一年沒生出來」是錯的(只看了 master);理由 (2) 「FAA 換 impl 即可」低估了——FAA 現成 validator 正是 (a) 要的、(c) 才需要改寫它。
|
||
|
||
---
|
||
|
||
### 決策 2:Client↔FAA 憑證傳遞與 Bucket 保護 — **v1.2 改寫(流程已 stage 實測確認)**
|
||
|
||
> **✅ v1.2 實測確認**:以下流程已用 stage 真實環境 + 真 secret + 真 OIDC user_id 跑通(見 §10.3 e2e)。token 確認為 MC 簽的 opaque `fdt_<base64url>`(120s、scope `files:download.read`、token_type `file_download`)。**FAA download endpoint 確切用法**(給 backend 實作):`GET {FAA}/files/{objectKey}`,token 放 **`Authorization: Bearer {fdt_token}`**(FAA `TryReadAccessToken` 只認 Authorization Bearer,**不認 query / 自訂 header**)。`objectKey` 必須與簽 token 時的 `object_key` **完全一致**(FAA validate boundary 檢查、不一致回 `object_key_mismatch`)。
|
||
|
||
採用決策 1**(a)** 後,下載流程具體化(token 由 **MC** 簽,不是 visionA 自簽):
|
||
|
||
1. Client(local-tool / browser)向 **visionA** 請求下載某 model(帶 visionA 使用者 session)。
|
||
2. visionA **先驗存取權限**(owner / share / workspace / role,見決策 3)。
|
||
3. 通過 → visionA **用 MC service token 打 MC `POST /file-access/download-tokens`**(Issue),帶 `tenant_id` / `user_id` / `file_id` / `object_key` / `method=GET` / `expires_in_seconds`,MC 回 opaque `fdt_<base64url>` token。
|
||
4. visionA 回給 Client「FAA 下載 URL + `fdt_` token」。
|
||
5. Client 帶 token 直接 `GET FAA /files/{object_key}`(**`Authorization: Bearer fdt_...`**——v1.2 實測:FAA 只認 Authorization Bearer、不認 `?access_token=` query;不經 visionA、不經 AWS)。
|
||
6. FAA `MemberCenterDelegatedDownloadTokenValidator` **打 MC `POST /file-access/download-tokens/validate`**(帶 FAA 自己的 MC service token、`instanceOptions.TenantId` + 從 URL path 取的 `ObjectKey` + `Method=GET`)驗 token + boundary(tenant/object_key/method)→ MC 回 `{active:true}` → FAA 回檔。
|
||
|
||
> 注意:`fdt_` 是 **opaque random token、不是 JWT** → FAA **無法本地驗簽**,每次下載一定要 call MC validate(這是 MC 刻意設計:可即時 revoke)。延遲多一跳 FAA→MC,但 FAA validator 有 service token cache(見 FAA validator source line 74-129),validate 本身是輕量 POST。
|
||
|
||
**防盜用 / 不曝露設計**:
|
||
|
||
| 風險 | 對策 |
|
||
|------|------|
|
||
| token 被截走重放 | 短 TTL(visionA Issue 時帶 `expires_in_seconds`,建議 60–300s,待裁決 Q2);MC 端 boundary 綁 `object_key` + `method`(截走也只能下載那一個檔、那個 method);MC 端可 `RevokedAt` 即時撤銷 |
|
||
| Bucket 位址曝露 | Client 只拿到 FAA URL(`/files/{object_key}`),object_key 是 FAA 內部 storage key、不是 S3/NAS 真實路徑 |
|
||
| CORS(browser 直連 FAA) | FAA 需設允許 visionA 前端 origin 的 CORS(待裁決 Q3:是否需瀏覽器直連;只 local-tool 直連無 CORS 問題)|
|
||
| FAA 對外網段 / HA | 見決策 5 |
|
||
| token 簽發濫用 | visionA 端對「打 MC Issue」做 rate limit + audit log(誰、何時、要下載哪個 model)|
|
||
|
||
#### ⚠️ object_key 三方對映斷層(v1.1 新增 — (a) 最大實作風險)
|
||
|
||
(a) 要求 visionA Issue 時帶的 `object_key` = FAA `GET /files/{key}` 的 key = MC validate 的 `object_key`,三方必須一致。但驗證現有 source 發現**斷層**:
|
||
|
||
| key 空間 | 值 | source |
|
||
|----------|-----|--------|
|
||
| **visionA model storage key** | `models/{userID}/{modelID}.nef` | `internal/api/models.go:235`(visionA 自己的 S3/LocalFS key)|
|
||
| **converter promote → FAA 的 object_key** | 由 `flow.go` 按命名規則組的 `TargetObjectKey`(FAA 內部 key) | `converter_client.go` PromoteReq.TargetObjectKey |
|
||
| **FAA 實際存放的 object_key** | converter PUT 進去的 key | FAA `PUT /files/{objectKey}` |
|
||
|
||
**斷層本質**:
|
||
- visionA model store 的檔案是走 `/api/models/init+finalize` 上傳到 **visionA 自己的 storage**(`models/{userID}/{modelID}.nef`),**從未經過 FAA**。
|
||
- 只有「轉檔結果 NEF」會經 converter promote 進 FAA(用另一套 `TargetObjectKey`)。
|
||
- **所以「model download (a) 直連 FAA」的前提——model 必須在 FAA 上、且 visionA 知道它的 FAA object_key——目前不成立**:
|
||
- 上傳類 model(SourceUploaded)根本不在 FAA 上,無法走 (a) 直連 FAA。
|
||
- 轉檔類 model(SourceConverted)若有 promote 進 FAA,visionA 需**記錄該 model 的 FAA object_key**(目前 `model.Model` struct 沒有 `faa_object_key` 欄位,只有 visionA 自己的 `StorageKey`)。
|
||
|
||
**(a) 要可行,必須補的 visionA 端工作(決策方向,細節由 backend agent 落地)**:
|
||
1. `model.Model` 加 `FAAObjectKey`(nullable)欄位,記錄 model 在 FAA 上的 key(promote 時寫入)。
|
||
2. 釐清「哪些 model 在 FAA 上」:只有經 converter promote 的才有 FAAObjectKey;純上傳的 model 要 (a) 直連 FAA 需先有「把上傳檔也 PUT 進 FAA」的路徑(目前沒有)。
|
||
3. download flow 用 `FAAObjectKey`(不是 `StorageKey`)去 Issue MC token + 組 FAA URL。
|
||
|
||
> **這個斷層是 (a) 比 (c) 多出的實作成本**,且影響「上傳類 model 能不能走 (a)」——須在 Q1 裁決時一併考慮(見待裁決 Q7)。
|
||
|
||
#### v1.0 原決策 2(visionA 自簽 JWT)保留供對照
|
||
|
||
> ~~採用決策 1(c):visionA 簽發短期 JWT(claim: file_key/sub/exp/scope)、FAA 用 visionA JWKS 本地驗簽。~~ v1.1 改為 MC 簽 opaque `fdt_` token + FAA 打 MC validate。
|
||
|
||
---
|
||
|
||
### 決策 3:B — 權限/分享資料模型(owner-only → share/workspace/role)
|
||
|
||
現況 `models.owner_user_id NOT NULL` 單一擁有者(事實 E)。最小可行變更,**不一次做到完整 RBAC**,只加「能分享 + 能用 workspace 群組 + 三種 role」:
|
||
|
||
**方案比較(最小可行 vs 完整):**
|
||
|
||
| 維度 | (B1) 只加 model_shares(per-model 直接分享)★第一階段推薦 | (B2) 加 workspaces + memberships + model 歸屬 workspace | (B3) 完整 ABAC/RBAC policy engine |
|
||
|------|--------------------------------------------------|----------------------------------------------|--------------------------------|
|
||
| 變更面 | 加 1 張 `model_shares(model_id, grantee_user_id, role, ...)` | 加 `workspaces` / `workspace_members` / `models.workspace_id` | 引入 policy 表 + evaluator |
|
||
| 滿足 pptx | 「分享給特定人」✓ | 「團隊/組織共用」✓ | 過度設計 |
|
||
| 工時 | 小 | 中 | 大 |
|
||
| 與現況落差 | 小(owner_user_id 保留,share 疊加) | 中 | 大 |
|
||
|
||
**推薦:分兩步 — 先 (B1),後 (B2);(B3) 不做。**
|
||
|
||
最小資料模型(對齊 `database.md §2.3` + `models` DDL,`owner_user_id` 保留不動):
|
||
|
||
```
|
||
-- 第一階段 (B1):per-model 分享
|
||
CREATE TABLE model_shares (
|
||
model_id UUID NOT NULL REFERENCES models(id),
|
||
grantee_user_id UUID NOT NULL REFERENCES users(id),
|
||
role TEXT NOT NULL, -- 'viewer' | 'editor'(owner 仍記在 models.owner_user_id)
|
||
granted_by UUID NOT NULL REFERENCES users(id),
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
PRIMARY KEY (model_id, grantee_user_id)
|
||
);
|
||
CREATE INDEX ON model_shares (grantee_user_id);
|
||
|
||
-- 第二階段 (B2):workspace 群組共用(待第一階段穩定後)
|
||
-- workspaces(id, owner_user_id, name, ...)
|
||
-- workspace_members(workspace_id, user_id, role)
|
||
-- models 加 nullable workspace_id(NULL=個人資產,非 NULL=workspace 資產)
|
||
```
|
||
|
||
**role 語意(三種,最小集)**:
|
||
- `owner`:`models.owner_user_id`,可刪除/改權限/分享。
|
||
- `editor`:可更新 model metadata、可 promote/覆蓋。
|
||
- `viewer`:只能 list/get/download。
|
||
|
||
**權限查驗點**:決策 2 第 2 步「visionA 先驗存取權限」就是查 `owner_user_id == user OR model_shares 命中 OR(B2 後)workspace_members 命中`,通過才簽 download token。`Repository.List` 要從「只用 owner_user_id 過濾」改成「owner ∪ shared ∪ workspace」。
|
||
|
||
> 此為**決策方向**,schema 細節由後續實作(backend agent)落地 migration,本 ADR 只定模型形狀。
|
||
|
||
---
|
||
|
||
### 決策 4:D — promote 收斂(stage OAuth 401)
|
||
|
||
事實 C/C':promote(converter→FAA PUT,OAuth client_credentials + scope `files:upload.write`)**prod 已 work**,只有 **stage 卡 401**,研判是**設定問題(stage OAuth client 設定 / FAA accepted scope),非設計問題**。
|
||
|
||
| 方案 | 說明 | 評估 |
|
||
|------|------|------|
|
||
| (D1) 修 stage OAuth 設定 ★推薦 | 對齊 stage converter 的 OAuth client_id/secret/scope 與 FAA 在 stage 接受的 scope(`files:upload.write`),讓 stage 跟 prod 一致 | promote 設計本身是對的(prod 已證明),不該為了 stage 設定問題去改架構 |
|
||
| (D2) 改走新存取路徑 | 把 promote 也改成決策 1(c) 的 token 模式 | **不推薦** — promote 是 server-to-server(converter→FAA),用 service JWT/OAuth 才對;decision 1(c) 的短期 download token 是給 Client 下載用,兩者用途不同,不該混 |
|
||
|
||
**推薦:(D1) 修 stage 設定,不動 promote 架構。** promote(上傳/server-to-server)與 download(Client 直連)是兩條不同的 auth 路徑,**正好對應 FAA 的 dual-auth 設計**(op=service JWT、download=delegated token),不要強行統一。
|
||
|
||
> promote 走 OAuth service token、download 走 visionA 簽的 delegated token — 這個分離是刻意的,符合事實 A 的 FAA dual-auth。
|
||
|
||
---
|
||
|
||
### 決策 4.5:與 ADR-016 converter 中轉的關係(v1.1 新增 — 並存,非取代)
|
||
|
||
ADR-016 讓 **轉檔結果 NEF download** 走 `converter GET /api/v1/jobs/{id}/result` + visionA stream 中轉(visionA 不直接打 FAA)。本 ADR (a) 是 **model library download** 走 Client 直連 FAA。兩者**是兩條不同用途的路徑、並存**:
|
||
|
||
| 路徑 | 用途 | 觸發點 | 來源 | 認證 |
|
||
|------|------|--------|------|------|
|
||
| **ADR-016 converter 中轉** | 下載**轉檔結果 NEF**(job-scoped、剛轉完還沒進模型庫)| `GET /api/conversion/{job_id}/download` | converter MinIO(7 天 expires_at)| visionA→converter API key |
|
||
| **ADR-017 (a) 直連 FAA** | 下載**模型庫 model**(持久資產、已在模型庫)| model download endpoint(待建)| FAA(持久)| MC `fdt_` token |
|
||
|
||
**為什麼並存而非取代**:
|
||
- ADR-016 的來源是 **converter MinIO(7 天就 GC)**——只適合「剛轉完、demand-trigger download」的短期場景,**不適合模型庫**(模型庫是持久資產,7 天後 converter MinIO 沒了)。
|
||
- ADR-017 (a) 的來源是 **FAA(持久)**——適合模型庫長期下載。
|
||
- 兩條路徑的「檔案在哪」不同(converter MinIO vs FAA)、生命週期不同(7 天 vs 持久),故並存。
|
||
|
||
**但有交集需釐清(待裁決 Q8)**:
|
||
- 「轉檔結果」可以「加到模型庫」(promote 進 FAA)。一旦加到模型庫,該 model 的 download 應走 (a)(FAA 持久),而非 ADR-016(converter MinIO 會過期)。
|
||
- **建議分流**:model 在模型庫內(有 FAAObjectKey)→ 走 (a);job 結果還沒進模型庫 → 走 ADR-016。兩者不互相取代,依「是否已是模型庫資產」分流。
|
||
- ADR-016 的 converter 中轉**保留不動**(轉檔結果 demand download 仍需要它);(a) 是**新增**模型庫的持久 download 路徑。
|
||
|
||
### 決策 5:HA / 頻寬(pptx 自列待解 G5)
|
||
|
||
| 問題 | 短期(本架構落地時) | 長期(標 Phase 後續,本 ADR 不深入) |
|
||
|------|---------------------|----------------------------------|
|
||
| HA(NAS / 公司網路掛掉全掛) | 接受單點風險 + 監控告警(FAA health check);關鍵 model 可在 converter MinIO 保留副本(事實 C'' NEF 雙存已是雛形)作為降級來源 | 移轉便宜雲端空間做 FAA 後端 / 多節點 FAA |
|
||
| 頻寬(下載走公司網路) | router 限流(pptx 已提);visionA 端對 download token 簽發做 rate limit | 移轉便宜雲端空間,下載改走雲端 egress(屆時重評「省流量」前提) |
|
||
|
||
**標記為後續 Phase**,本 ADR 先讓核心存取路徑(決策 1–4)跑通;HA/頻寬不阻擋第一階段,但須在 progress 留風險登記。
|
||
|
||
---
|
||
|
||
## 5. 建議落地順序
|
||
|
||
依賴關係:~~決策 1(a) 的跨團隊部署是地基~~(**v1.2:跨團隊部署已 stage e2e 實測完成、不再是地基/不確定性**),C(下載)依賴 visionA 端實作;B(權限)是下載前的查驗點;D 與 B/C 無依賴可平行。
|
||
|
||
> **v1.2 改寫 P0**:v1.1 的 P0 是「MC merge+部署 + scope 註冊 + FAA stage 設定」這條跨團隊部署鏈、標為 (a) 最大不確定性。**v1.2:這條鏈已用 stage 真實環境 e2e 實測打通(§10)、跨團隊 blocking 歸零。** P0 縮減為「visionA 端實作」+「(正式上線前)請 MC 給 visionA 專屬 file_api client」。短期 PoC 直接共用 FAA service client `4242ba63...`(已實測可拿 token)。
|
||
|
||
| 階段 | 內容 | 依賴 | 可平行? |
|
||
|------|------|------|---------|
|
||
| **P0(跨團隊部署)✅ v1.2 已驗證完成** | ~~① MC merge+部署 stage ② scope 註冊 ③ FAA stage 設定~~ **三項已 stage e2e 實測通過(§10)**。剩餘 P0 = 僅「正式上線前請 MC 配發 visionA 專屬 usage=file_api client」(短期 PoC 共用 `4242ba63...`,非 blocking)| 無 | — |
|
||
| **P0.5(visionA 端,現為起點)** | **B2**:復活 MC client(打 MC `/oauth/token` + 打 MC Issue 簽 fdt,逆轉 ADR-016 §5;比原版單純——token 由 MC 簽、不自簽)+ .env 設定(client `4242ba63...` / MC base url / FAA base url / tenant `732270c0...`)。**B1**:`model.Model` 加 `FAAObjectKey` 欄位(解 object_key 斷層,見決策 2 + §10.4)| 無(P0 已完成)| 與 P1a/P1b 平行 |
|
||
| **P1a** | D — 修 stage OAuth 設定(決策 4 D1)。**v1.2 線索**:`4242ba63...` 有 `files:upload.write`、promote 卡的 401 很可能同類問題(converter 用的 client 沒 upload.write)→ 可一併驗證 | 無 | **與 P0.5/P1b 平行** |
|
||
| **P1b** | B 第一階段 — model_shares 表 + 權限查驗(決策 3 B1)| 無 | **與 P1a 平行** |
|
||
| **P2** | C — visionA 打 MC Issue + model download flow(決策 1a + 決策 2,FAA 用法見決策 2 v1.2 確認框);第一階段 (a) 只支援轉檔→promote 類 model(object_key 斷層,§10.4 B1)| P0.5 + P1b | 接 P0.5/P1b |
|
||
| **P3** | B 第二階段 — workspaces/workspace_members(決策 3 B2)| P1b 穩定後 | 獨立 |
|
||
| **P4(後續 Phase)** | HA / 頻寬(決策 5 長期方向)| 不阻擋前面 | 獨立 |
|
||
|
||
> **~~fallback 觸發點~~(v1.2:跨團隊部署已通、fallback 不再需要)**:v1.1 設想「P0 卡住則 fallback (c)」。v1.2 P0 已 stage e2e 通、無卡點,**(c) 不動用**。僅保留「正式上線改用 visionA 專屬 file_api client、若 MC 端無法配發時」的理論退路。
|
||
|
||
不寫 task 級細節;各階段的實作 task 拆分由 backend agent 在進開發時做。
|
||
|
||
---
|
||
|
||
## 6. 待使用者裁決清單
|
||
|
||
| # | 待裁決項 | 推薦選項 | 理由 |
|
||
|---|---------|---------|------|
|
||
| **Q1(v1.2:已確認走 (a))** | 決策 1 認證方向:**(a) MC delegated token** vs (c) visionA 自簽(fallback) | **✅ 已確認走 (a)** — stage e2e 實測打通(§10),(c) 無需動用 | MC stage 已部署 + scope 機制 + FAA validate 鏈全部 e2e 實證;符合使用者 follow pptx 意圖。(c) 僅留理論退路 |
|
||
| **Q1.5(v1.2:已實測 P0 可行)** | (a) 的 P0 跨團隊前置 checklist 能否打通 | **✅ 已實測通過** — MC/FAA stage 已部署、scope 用 FAA client `4242ba63...` 實測可拿 token、e2e 全綠(§10)| 原「5/9 撞 invalid_scope」風險已實證可繞過(共用 FAA client)。跨團隊 blocking 歸零 |
|
||
| **Q2** | download token 有效期(visionA Issue 時帶的 `expires_in_seconds`)| **60–300 秒**(傾向 120s)| 夠 Client 發起下載、又短到截走也很快失效 |
|
||
| **Q3** | 是否需支援**瀏覽器**直連 FAA(牽涉 CORS),或只 local-tool 直連 | 先**只 local-tool**(無 CORS),瀏覽器需求出現再加 FAA CORS | 縮小第一階段範圍 |
|
||
| **Q4** | B 權限模型做到哪 | **先 B1(model_shares),B2 排後續,B3 不做** | 最小可行先上 |
|
||
| **Q5** | promote stage 401 修法 | **D1 修 stage 設定,不動 promote 架構** | prod 已證明設計對 |
|
||
| **Q6** | HA/頻寬是否阻擋第一階段 | **不阻擋,標後續 Phase + 風險登記** | 先讓核心存取路徑跑通 |
|
||
| **Q7(v1.1 新增)** | object_key 斷層:上傳類 model(不在 FAA 上)能否走 (a) 直連 FAA | **第一階段 (a) 只支援「轉檔→promote 進 FAA 的 model」(有 FAAObjectKey);純上傳 model 的 FAA download 排後續**(需先有「上傳檔也 PUT 進 FAA」的路徑)| 純上傳 model 目前只在 visionA 自己 storage、不在 FAA,無法直連;避免第一階段範圍爆炸 |
|
||
| **Q8(v1.1 新增)** | model download 走 (a) 直連 FAA vs 沿用 ADR-016 converter 中轉的分流 | **已在模型庫(有 FAAObjectKey)→ (a);job 結果還沒進模型庫 → ADR-016**(兩者並存)| ADR-016 來源 converter MinIO 7 天就 GC、不適合模型庫持久資產;(a) 來源 FAA 持久 |
|
||
| **Q9(v1.2:已同意復活 MC client)** | 是否接受「復活 MC service token client」(逆轉 ADR-016 §5 剛砍的 mc_token_client.go)| **✅ 已同意接受**((a) 必要成本;邏輯比原版單純:打 MC `/oauth/token` 拿 service token + 打 MC Issue 簽 fdt,token 由 MC 簽、不需自簽)| (a) 要 visionA 打 MC Issue,必須有 MC service token;使用者已同意 |
|
||
| **Q10(v1.2 新增)** | 是否接受「短期共用 FAA service client `4242ba63...` 做 PoC、正式上線前換 visionA 專屬 usage=file_api client」| **接受(使用者已傾向)** — 短期解 blocking、正式上線換專屬降技術債 | `4242ba63...` 是現成 usage=file_api client(實測可拿 download.delegate token);共用違反 MC「secret 不共用」規範(技術債,見 R1),正式上線換專屬 client。詳見 §7 R1 + §10.2 |
|
||
|
||
---
|
||
|
||
## 7. 後果
|
||
|
||
### 正面 — **v1.1 改寫**
|
||
- **模型下載有可行路徑、且符合 pptx 原圖((a))**:不需 visionA 自建簽發 + JWKS,MC/FAA source 已就緒。
|
||
- 滿足 pptx 兩大訴求:Bucket 不曝露 + 每次驗權限 + 下載不經 AWS。
|
||
- **FAA/MC 端零程式碼改動**(只需部署 + 設定 + scope 註冊)——比 (c)「請 warrenchen 改寫 FAA validator」輕。
|
||
- **token 安全性最強**:MC 簽 opaque `fdt_` token,支援 boundary + revoke + 自訂 TTL,比 visionA 自簽 JWT(無法 revoke)完整。
|
||
- B 權限分階段,先解 owner-only 痛點,不過度設計。
|
||
|
||
### 負面(接受的取捨)— **v1.1 改寫**
|
||
- **visionA 要復活 MC service token client**(逆轉 ADR-016 §5 剛砍的 mc_token_client.go)——但邏輯比原版單純(只需 service token cache + 打 Issue,不需自簽 delegated token)。
|
||
- **(a) 依賴 MC 線上 validate**:每次 FAA download 多一跳 FAA→MC(FAA validator 有 token cache 緩解,但 MC 成驗證單點)。
|
||
- **object_key 斷層成本**:`model.Model` 要加 `FAAObjectKey`、釐清「哪些 model 在 FAA 上」(見決策 2)。
|
||
- **跨團隊部署不可控**:(a) 的 P0 依賴 warrenchen 排程(merge/部署/scope 註冊)。
|
||
- HA/頻寬留後續 Phase,第一階段 NAS/公司網路是單點。
|
||
|
||
### 風險 — **v1.1 改寫**
|
||
- **R1(v1.2 改寫——原「scope 註冊最高風險」已 e2e 實證解除,改為共用 client 的技術債)**:v1.1 的 R1(「scope 沒在 MC 註冊、5/9 撞 invalid_scope」)已用 stage e2e 實測解除(§10:MC scope 機制驗證通過、FAA client `4242ba63...` 實測可拿 `files:download.delegate` token)。**v1.2 新的 R1 = 短期共用 FAA service client `4242ba63...` 的技術債**:(i) 違反 MC source 明訂「OAuth client 禁止混用 usage、secret 不共用」;(ii) FAA 的 client secret 進 visionA `.env` → 擴大 secret 洩漏面(一份 secret 同時被 FAA 與 visionA 持有,任一邊洩漏波及兩個服務)。**緩解:正式上線前請 MC 配發 visionA 專屬 usage=file_api client(換掉 `4242ba63...`),把 secret 邊界收回 visionA 自己**(待裁決 Q10、合規清單追蹤)。實測共用實際上 e2e 可跑通是因 `4242ba63...` 本就是 usage=file_api client、自動帶 5 個 files scope。
|
||
- **R2(v1.1 新增)**:object_key 三方斷層(決策 2)——上傳類 model 不在 FAA 上,第一階段 (a) 只能支援轉檔→promote 的 model。若使用者期待「所有 model 都能直連 FAA」,範圍大幅擴張。
|
||
- **R3(v1.1 改寫)**:(a) 跨團隊部署任一卡住 → 整條 (a) 不通。緩解:保留 (c) 為 fallback;P0 設可接受期限,逾期 fallback。
|
||
- **R4(opaque token 限制)**:`fdt_` 是 opaque、FAA 無法本地驗,每次 download 必 call MC validate。MC validate 慢/掛 → 所有 model download 失敗。緩解:FAA validator service token cache + MC validate SLA 監控。
|
||
- **R5(pptx 矛盾,原 R3)**:pptx「只傳網址讓 FAA 去抓」上傳省流量,但來源(converter MinIO)也在公司網路,HA/頻寬風險與下載同源。
|
||
- **R6(原 R4)**:NEF 在 converter MinIO(7天)+ FAA 雙存,長期雙存一致性與清理策略未定,須在 P2 釐清。
|
||
- **R1'(僅 fallback 回 (c) 才適用)**:(c) 把 token 簽發收回 visionA,**JWKS 私鑰外洩 = 全模型庫可被簽 token 下載**,須 secret 管理 + key rotation。(a) 無此風險(token 由 MC 簽、可 revoke)。
|
||
|
||
---
|
||
|
||
## 8. 合規性 — **v1.2 改寫**
|
||
- [x] **(a) 的 P0 跨團隊部署 checklist** — ✅ v1.2 已 stage e2e 實測完成(MC/FAA stage 已部署、scope 機制驗證、Issue→fdt→FAA validate 鏈全綠,§10),跨團隊 blocking 歸零
|
||
- [ ] **(正式上線前)請 MC 配發 visionA 專屬 usage=file_api client** 換掉短期共用的 FAA client `4242ba63...`(R1 技術債、Q10)
|
||
- [x] 與使用者確認 Q1 / Q1.5 / Q9 裁決 — ✅ 已確認走 (a) + 同意復活 MC client;新增 Q10(共用 client PoC)使用者已傾向接受
|
||
- [ ] 與 backend agent 確認 object_key 斷層解法(`model.Model` 加 `FAAObjectKey`、上傳類 model 範圍)
|
||
- [ ] 與 security agent 評估 R4(MC validate 單點 / SLA);若 fallback (c) 則評估 R1'(JWKS 私鑰)
|
||
- [ ] 成本影響:第一階段無新雲端資源(沿用現有 FAA + NAS + MC),主要是 visionA 開發工時 + 跨團隊部署協調
|
||
|
||
---
|
||
|
||
## 9. (a) 端到端可行性驗證(v1.1 新增)
|
||
|
||
本節記錄改推 (a) 前所做的 source 驗證證據與三端缺口清單。
|
||
|
||
### 9.1 source 證據(已就緒的部分)
|
||
|
||
| 端 | 證據 | 結論 |
|
||
|----|------|------|
|
||
| **MC develop** | commit `e77fdec`(FileAccessDownloadToken entity + migration):`POST /file-access/download-tokens`(Issue, `[Authorize(Policy="FilesDownloadDelegate")]`,回 `fdt_<base64url>` + scope `files:download.read` + 自訂 `expires_in_seconds`,token hash 存 DB);`POST /file-access/download-tokens/validate`(Validate, `[Authorize(Policy="FilesDownloadRead")]`,驗 TokenHash + RevokedAt + ExpiresAt + boundary)。commit `5f32452` AuthResourceRegistry 做 audience/scope mapping。**master 尚未 merge。** | Issue + Validate **已實作**;token 為 opaque(非 JWT)→ 必 call MC validate |
|
||
| **FAA(master,working tree 已驗)** | `MemberCenterDelegatedDownloadTokenValidator.cs`:`ValidateAsync` 用 `GetServiceAccessTokenAsync`(FAA 自己拿 MC service token,`client_credentials` + `_options.Scope`)→ `PostAsJsonAsync(_options.DownloadTokenValidationPath, payload)` 打 MC。payload = `{token, tenant_id, file_id, object_key, method}`、response = `{active, tenant_id, user_id, file_id, object_key, method, expires_at}`。`Program.cs:33` 註冊此 validator;`Program.cs:184` `GET /files/{**objectKey}` 用它驗 + boundary 檢查(tenant_mismatch / object_key_mismatch / method_mismatch)。 | FAA validator 的 payload/response 形狀**與 MC develop validate 契約吻合** → 配套設計,FAA 端**不需重寫** |
|
||
| **visionA(已驗)** | `internal/conversion/`:mc_token_client.go **確實已不存在**(ADR-016 §5 砍除生效)。download 現走 converter `GetResult`(`converter_client.go:998`)+ API key。model 上傳走 `/api/models/init+finalize` 進 visionA 自己 storage(`models.go:235` `StorageKey=models/{userID}/{modelID}.nef`)。 | visionA 端要 (a) 須**復活 MC service token client** + 新增打 MC Issue 邏輯 |
|
||
|
||
### 9.2 三端缺口清單((a) 端到端「還缺什麼」)
|
||
|
||
| 端 | 缺口 | 誰做 | 風險 |
|
||
|----|------|------|------|
|
||
| **MC** | ① develop 尚未 merge master + 部署 stage | warrenchen | 排程不可控 |
|
||
| **MC** | ② visionA service client(`23605e14...`)的 `files:download.read/delegate` scope **是否已在 MC AuthResourceRegistry 註冊**未確認(ADR-014 line 35 只證明「visionA 文件列了」,非「MC 端註冊了」);Issue 的 `[Authorize(Policy="FilesDownloadDelegate")]` 需 visionA token 帶對應 claim | warrenchen | **最高(5/9 撞過 invalid_scope)** |
|
||
| **FAA** | ③ FAA stage 是否部署 + `MemberCenterOptions`(BaseUrl/ClientId/ClientSecret/Scope/DownloadTokenValidationPath)是否設好未確認;FAA 自己也要有 MC service token(scope 待確認)| warrenchen | 中 |
|
||
| **visionA** | ④ 復活 MC service token client(逆轉 ADR-016 §5)+ 打 MC Issue | jimchen / backend agent | 低(自己可控)|
|
||
| **visionA** | ⑤ object_key 斷層:`model.Model` 加 `FAAObjectKey`、釐清上傳類 model 不在 FAA 上的範圍(見決策 2)| jimchen / backend agent | 中(範圍影響)|
|
||
| **三方** | ⑥ boundary 對齊:visionA Issue 帶的 object_key = FAA URL key = MC validate object_key(見決策 2 斷層分析)| 三方協調 | 中 |
|
||
|
||
### 9.3 驗證結論
|
||
|
||
- **source level:(a) 通**(MC develop Issue+Validate 已寫、FAA validator 配套、契約吻合)。**v1.0 判 fictional 是因只 grep master。**
|
||
- **部署 level:(a) 未通**,缺 §9.2 六項,其中 ② scope 註冊是最高風險(warrenchen 端、5/9 同類事故)。
|
||
- **visionA 端額外成本**:復活 MC service token client(逆轉 ADR-016)+ 解 object_key 斷層(加 FAAObjectKey)。
|
||
- **(c) 在新事實下反而更重**:FAA 現成 validator 正是 (a) 要的;(c) 要請 warrenchen 改寫成本地驗 JWKS,是「改既有正確配套」。
|
||
- **最終建議:推 (a),但 P0 step ②③ 須先跟 warrenchen 確認可行(Q1.5),逾期 fallback (c)。**
|
||
|
||
> **⚠️ v1.2 後記**:§9 是 v1.1 時的「source level 驗證 + 待跨團隊驗證」紀錄。**§9.2 六項缺口中的 ①②③(MC merge/部署、scope 註冊、FAA stage 設定)已於 v1.2 用 stage 真實環境 e2e 實測全部通過**(見 §10)。§9.3「部署 level 未通」結論已被 §10 推翻。④⑤⑥(visionA 端實作 + object_key 斷層 + boundary 對齊)轉為 §10.4 的 visionA 端 checklist。
|
||
|
||
---
|
||
|
||
## 10. (a) 端到端 stage 實測證據(v1.2 新增 — 本 ADR 最有價值的部分)
|
||
|
||
本節記錄 v1.1 → v1.2 之間,用 **stage 真實環境 + 真 secret + 真 OIDC user_id** 把整條 (a) 鏈路打通的實證。**這把 (a) 從「待驗證的可行性」升級為「已實證可行」。**
|
||
|
||
### 10.0 環境與憑證(使用者由 MC team 取得)
|
||
|
||
| 項目 | 值 |
|
||
|------|-----|
|
||
| MC stage Web | `https://stage-9527.innovedus.com:7880/` |
|
||
| MC stage API | `https://stage-9527.innovedus.com:7850/` |
|
||
| FAA stage | `https://stage-9527.innovedus.com:5081/` |
|
||
| visionA Login client(web_login) | `b8093fea1a504a5d8f0e04bee9f78f2e`,callback `https://stage-9527.innovedus.com:9527/api/auth/callback`(**注意:MC team 訊息少寫 `/api`,以 visionA 現用含 `/api` 的為準、勿改**)|
|
||
| visionA「Login 以外 API call」共用 client | `23605e14a2c64660abd97e29963d8d58`(client_credentials)|
|
||
| **FAA service client(PoC 共用)** | `4242ba63099d4f318dd3f143d27ef4c5`,tenant `732270c0-449c-489c-bfad-321e9bf89b3d`,scopes `files:upload.write files:metadata.read files:delete files:download.delegate` |
|
||
| 真實 OIDC user_id(從 `/api/auth/me`) | `b5332e51-c394-45c7-a28a-83e6da127ba9` |
|
||
|
||
### 10.1 MC 部署確認(推翻「只看 git master 判 fictional」)
|
||
|
||
- `POST /file-access/download-tokens`(Issue)與 `/validate` 在 stage 回 **401(非 404)→ endpoint 已部署**。對照亂打路徑回 404。
|
||
- → **推翻 v1.0/v1.1「只看 git master 判 fictional」**:stage 實際版本已含 develop 的 download token 功能(即 §9.2 缺口 ①「develop 尚未 merge master」對 stage 不成立——stage 跑的版本已有此功能)。
|
||
|
||
### 10.2 scope 模型診斷(MC usage→scope 模型)
|
||
|
||
- MC discovery `scopes_supported` 含 `files:download.read/delegate/upload.write/metadata.read/delete`。
|
||
- **MC 用 usage→scope 模型**(`AuthResourceRegistryService`):`file_api` 這個 **usage 類型**(不是現成 client)自動帶 5 個 files scope。`file_api` 強制 **confidential + client_credentials + 綁 tenant_id**。source 明訂「**OAuth client 禁止混用 usage、secret 不共用**」(→ R1 技術債的根據)。
|
||
- **visionA 共用 client `23605e14...` 實測**:所有 files scope 全部 `not allowed`;`converter:job.*` 是 invalid(**MC 根本沒這 scope**,呼應 converter 已改 API key)。→ **共用 client 對 file_api 行不通**(規範禁止混用 usage)。
|
||
- **FAA client `4242ba63...` 實測**:能拿 `files:download.delegate` token,`aud=file_access_api` → 它就是個 **usage=file_api 的 client**(所以 PoC 直接共用它可行)。
|
||
|
||
### 10.3 🏆 (a) 端到端 e2e 全綠(stage 真實環境、真 secret、真 user_id)
|
||
|
||
| Step | 動作 | 結果 |
|
||
|------|------|------|
|
||
| **Step 1** | `4242ba63...` 打 MC `/oauth/token`(client_credentials)拿 `files:download.delegate` token | ✅ 拿到 |
|
||
| **Step 2** | 帶該 token + 真 user_id 打 MC `POST /file-access/download-tokens`(Issue)| ✅ **HTTP 200,簽出 `fdt_...` token**(token_type=`file_download`、120s、scope=`files:download.read`、tenant/user/object_key 正確回填)|
|
||
| **Step 3** | 拿 `fdt_` token 以 `Authorization: Bearer` 打 FAA `GET /files/{objectKey}` | ✅ **HTTP 404 `file_not_found`**(= 認證 + MC validate 全過、FAA 去 bucket 找檔、**只因 `test/probe.nef` 是假 key 不存在**;換真實檔就回檔案內容)|
|
||
|
||
→ **B3(tenant 一致)、B4(user 是 MC 真實 user)、FAA validate 鏈,全部實證通過。** 404 是「檔案不存在」而非「認證失敗」——這正是認證鏈全綠的證明。
|
||
|
||
### 10.4 剩餘 visionA 端 blocking(backend agent 接下來的工作)
|
||
|
||
| # | 項目 | 說明 |
|
||
|---|------|------|
|
||
| **B1(最關鍵)** | **object_key 斷層** | visionA model `StorageKey=models/{userID}/{modelID}.nef`(visionA 自己 storage)≠ FAA object_key。**上傳類 model(SourceUploaded)根本不在 FAA → 第一階段 (a) 只支援轉檔→promote 類 model**。`model.Model` 要加 `FAAObjectKey` 欄位 + promote 時寫入。download flow 用 `FAAObjectKey`(非 `StorageKey`)組 FAA URL + Issue token |
|
||
| **B2** | **visionA 加 MC client code** | 加「打 MC `/oauth/token` + 打 MC Issue 簽 fdt token」的 client code(**逆轉 ADR-016 砍除的 mc_token_client,但更單純:不自簽、token 由 MC 簽**)+ .env 設定(client `4242ba63...` / MC base url / FAA base url / tenant `732270c0...`)|
|
||
| **D(連帶)** | **promote 401 可一併驗證** | `4242ba63...` 有 `files:upload.write` → promote 卡的 401 **很可能同類問題**(converter 用的 client 沒 `upload.write`)。標為「可一併驗證」 |
|
||
|
||
**FAA download endpoint 確切用法(給 backend 實作)**:
|
||
- `GET {FAA}/files/{objectKey}`,token 放 `Authorization: Bearer {fdt_token}`(FAA `TryReadAccessToken` **只認 Authorization Bearer**,不認 query / 自訂 header)。
|
||
- FAA 自己用 `IDelegatedDownloadTokenValidator`(=`MemberCenterDelegatedDownloadTokenValidator`)拿 token 打 MC validate,帶 `instanceOptions.TenantId` + `ObjectKey`(從 URL path 取)+ `Method=GET`。
|
||
- `objectKey` 必須與簽 token 時的 `object_key` **完全一致**(FAA validate boundary 檢查、不一致回 `object_key_mismatch`)。
|
||
|
||
> **⚠️ v1.3 修正(2026-06-27)**:上面這行「FAA `TryReadAccessToken` **只認 Authorization Bearer**,不認 query」**已過時、被 §11 推翻**。重讀 FAA repo 最新 master(`~/file_access_agent`)發現 `TryReadAccessToken`(`src/FileAccessAgent.Api/Program.cs:324-341`)**先讀 `Authorization: Bearer`、沒有才讀 `?access_token=` query string**——即 FAA **原生支援 query-string token**。v1.2 的「只認 Bearer」結論是當時測 stage 沒測 query path 造成的誤判。下載對接因此改為 **query-string token + 瀏覽器 redirect 導航**(避開 CORS preflight),決策與落地規格見 **§11**。
|
||
|
||
---
|
||
|
||
## 11. v1.3:瀏覽器下載改 query-string token + redirect(推翻決策 2 / §10.4 的「fetch + Bearer」)
|
||
|
||
> **狀態**:Accepted(2026-06-27)。本節 supersede 決策 2 第 5 步、§10.4 的「FAA 只認 Authorization Bearer」結論,以及前端 `model-download.ts` 現行的 `fetch + Bearer + blob` 實作。**只改「Client→FAA 那一跳怎麼帶 token」,不改認證鏈本體**(MC 簽 fdt / FAA 打 MC validate / boundary 檢查全部不變)。
|
||
|
||
### 11.1 背景(為什麼要改)
|
||
|
||
v1.2 決策 2 / §10.4 要求前端用 `fetch(faaUrl, { headers: { Authorization: 'Bearer fdt_...' } })` + `response.blob()` + 動態 anchor 觸發下載。stage 實測(截圖實錘)顯示:
|
||
|
||
- 前端(`9527`)`fetch` 跨 origin 直連 FAA(`5081`)+ 帶自訂 `Authorization` header → 瀏覽器判定為 **non-simple request**、先送 **CORS preflight `OPTIONS`**。
|
||
- FAA 沒設 CORS(無 `AddCors` / `UseCors`)、`OPTIONS /files/...` 回 **405** → 瀏覽器報 **CORS error** → 下載死。
|
||
- 根因定位見 `.autoflow/04-architecture/download-cors-405-diagnosis.md`。
|
||
|
||
**FAA team 立場**:不設 CORS。因為 FAA 設計就是「token 放 query string + redirect 導航下載」——這條路徑天生不觸發 CORS preflight(瀏覽器導航不是 fetch、無自訂 header)。
|
||
|
||
### 11.2 FAA repo 實證(修正 v1.2 的誤判)
|
||
|
||
| 事實 | 證據(`~/file_access_agent` 最新 master) |
|
||
|------|------------------------------------------|
|
||
| FAA download token **支援放 query string**,參數名 `access_token` | `src/FileAccessAgent.Api/Program.cs:324-341` `TryReadAccessToken`:先讀 `Authorization: Bearer`、`else` 讀 `Request.Query["access_token"]` |
|
||
| FAA 官方 test page 的正解就是 query + redirect | `src/FileAccessAgent.TestSite/Controllers/HomeController.cs:255-282` `DownloadFileDirect`:組 `/files/{encodedObjectKey}?access_token={Uri.EscapeDataString(token)}` 後 `Redirect()` 導航 |
|
||
| FAA 確實沒設 CORS | 全 repo 無 `AddCors` / `UseCors` |
|
||
| objectKey encode 規則 | segment-wise URL encode(每段各自 escape、保留 `/`);token 用 `Uri.EscapeDataString` |
|
||
|
||
→ **query-string token 是 FAA 原生支援、且是官方建議用法**。改用它 = 對齊 FAA 設計,不是 workaround。
|
||
|
||
### 11.3 決策 A:token 放 URL 的責任切分 — **推薦「乙:後端組好完整 URL」**
|
||
|
||
| 維度 | 甲:後端回 `{download_url, token, expires_at}`、**前端**拼 `?access_token=` | 乙:**後端**直接回已含 `?access_token=` 的完整 URL ★推薦 |
|
||
|------|----------------------------------------------------------------------|---------------------------------------------------------|
|
||
| token escape 責任 | 前端做 `encodeURIComponent(token)`(多一處易錯點) | 後端 `url.QueryEscape(token)`(與 objectKey escape 同一處、一致) |
|
||
| objectKey 知識 | 前端不需碰(download_url 已含 objectKey);但要懂「在尾巴 append query」 | 前端完全不碰 URL 組裝、拿到直接導航 |
|
||
| 職責清晰度 | URL 由前後端**各組一半**(後端組 path、前端組 query)→ 切割點尷尬 | URL 完全由後端產生 = **單一真實來源**、token 不外露給前端邏輯層處理 |
|
||
| API 契約衝擊 | **不變**(仍回 3 欄)→ 後端 test 不用大改 | **變**:`download_url` 內含 token;`token` / `expires_at` 欄位處置見下 |
|
||
| 安全(token 出現處) | token 在前端 JS 變數 + URL;前端任何 log 都可能印到 | token 由後端直接埋進 URL;前端只當「不透明連結」傳給瀏覽器,**不進前端邏輯層處理** |
|
||
| 既有 backend test | 幾乎不動(仍驗 3 欄) | 需改 `models_download_test.go`:驗 `download_url` 含 `?access_token=<escaped token>` |
|
||
|
||
**推薦乙、理由**:
|
||
|
||
1. **URL 是單一真實來源**:FAA 下載 URL 的「path(objectKey segment-escape)+ query(token escape)」是一套 FAA encode 規則,由**同一端(後端)**用同一套邏輯組完最不易出錯。甲案把這套規則切兩半(後端組 path、前端 append query),切割點本身就是 bug 溫床。
|
||
2. **token 不下放到前端邏輯層**:乙案前端把 `download_url` 當「不透明可導航連結」,token 不再是前端要 `encodeURIComponent` / 存變數 / 可能誤印 log 的東西。降低 token 在前端誤洩風險。
|
||
3. **對齊 FAA 官方用法**:FAA test page(§11.2)就是 server 端組好完整 URL 再 redirect。乙案 = 同模式。
|
||
4. **甲案唯一好處(API 契約不變)價值低**:契約本來就要改(fetch→redirect 是行為大改、前端 test 一定要重寫),保留 3 欄結構省不了多少。
|
||
|
||
**乙案的 API 契約處置(給 backend,重要)**:
|
||
|
||
- `download_url`:改為 `{FAABaseURL}/files/{segmentEscapedObjectKey}?access_token={queryEscapedToken}`(在現有 §10.4 / `models.go:550` 組好的 URL 尾巴 append `?access_token=`)。
|
||
- `token` 欄位:**保留但標記 deprecated**(向下相容——舊版 local-tool / 既有呼叫方可能還讀它)。值仍回原始 `fdt_` token。前端**改為不使用它**(不再需要拼 header / 拼 URL)。**不要直接移除欄位**,避免破壞既有 consumer。
|
||
- `expires_at`:**保留**(前端 / UI 仍可用來顯示「連結有效期」提示,雖然第一階段未用)。
|
||
- 即:response 結構**只是 `download_url` 內容變化 + `token` 轉 deprecated**,欄位不刪 → 對既有 consumer 向下相容。
|
||
|
||
### 11.4 決策 B:前端導航方式 — **推薦「動態 `<a download href>` click」**
|
||
|
||
| 方式 | 評估 |
|
||
|------|------|
|
||
| **動態 `<a href download>` + click** ★推薦 | 明確語意「下載」;`download` 屬性提示瀏覽器存檔而非導頁;同分頁不會把目前 SPA 畫面導走 |
|
||
| `window.location.href = url` | 會嘗試把**目前分頁**導去該 URL。若 FAA 回 `Content-Disposition: attachment`,多數瀏覽器會轉成下載、不離開頁面;但若 header 缺失或瀏覽器行為差異,可能變成「整頁導去 FAA」破壞 SPA。風險較高 |
|
||
|
||
**推薦動態 `<a download href>`、理由**:跨 origin 時 `<a download>` 的 `download` 屬性檔名提示會被瀏覽器忽略(安全限制),但**「觸發下載而非導頁」的行為仍比 `window.location` 穩**——尤其搭配 FAA 的 `Content-Disposition: attachment`,瀏覽器會以下載處理且不離開當前頁。可重用既有 `triggerBlobDownload` 的 anchor 建立/click/remove 樣式(但 href 改成 FAA URL、不再是 blob object URL,且**不需要 `URL.revokeObjectURL`**)。
|
||
|
||
**檔名處理(重要簡化)**:
|
||
|
||
- FAA 下載回 `Content-Disposition: attachment; filename=...`,**瀏覽器會用 FAA 給的檔名**。
|
||
- 跨 origin 導航時 `<a download="xxx">` 的 `xxx` **會被瀏覽器忽略**(跨 origin 安全限制)→ 前端設不設 `download` 屬性的檔名都不影響結果。
|
||
- → **`deriveDownloadFilename` 在 redirect 方案下失去作用**(它原本是給 blob 方案命名用的;blob 是 same-origin object URL、`download` 屬性有效)。redirect 方案下檔名完全由 FAA 的 `Content-Disposition` 決定。
|
||
- **建議**:移除 `deriveDownloadFilename` 的呼叫(store 不再需要),函式本身可留著標 deprecated 或一併刪(由 frontend agent 判斷,傾向刪以免死碼)。`<a>` 仍可設 `download=""`(空值)純粹表達「這是下載意圖」,但不依賴它命名。
|
||
|
||
### 11.5 決策 C:objectKey / URL encode 釐清
|
||
|
||
- **後端現況**:`models.go:550` `downloadURL = {FAABaseURL}/files/{escapeFAAObjectKey(objectKey)}`,`escapeFAAObjectKey` 已做 segment-wise `url.PathEscape`、保留 `/`(`models.go:570-576`)——**與 FAA encode 規則對齊、已組好含 objectKey 的完整 path、不含 query**。
|
||
- **結論**:採乙案後,**前端完全不碰 objectKey encode**。後端在現有 URL 尾巴 append `?access_token={url.QueryEscape(token)}` 即可(用 `url.QueryEscape` 對齊 FAA test page 的 `Uri.EscapeDataString`)。前端只是把這個完整 URL 丟給 `<a href>`。
|
||
|
||
### 11.6 決策 D:安全考量(token 進 URL)— **結論:可接受 + 輕量緩解**
|
||
|
||
token 放 query string 會落在:瀏覽器歷史、FAA server access log(可能)、`Referer`(導航到 FAA,但 FAA 是 token 的目的地、非第三方)。
|
||
|
||
**風險評估(結論:可接受)**:
|
||
|
||
| 因素 | 評估 |
|
||
|------|------|
|
||
| token TTL | **120s 短效**(§10.3 實測 `expires_in_seconds`)→ 落歷史 / log 後很快失效 |
|
||
| token 範圍 | **單檔 + 單 method(GET)boundary**(MC validate 綁 object_key + method)→ 截走只能下載那一個檔 |
|
||
| token 形態 | **opaque `fdt_`**(非 JWT、不含可解析的 user/權限資訊)+ MC 可 `RevokedAt` 即時撤銷 |
|
||
| 使用次數 | 一次性下載場景(非長期憑證) |
|
||
|
||
→ **這正是 FAA 設計 query-string token 時就接受的取捨**(FAA 官方 test page 即如此用)。短 TTL + 單檔 boundary + opaque + 可 revoke,四重特性使「token 落 URL」風險可接受。
|
||
|
||
**輕量緩解(不過度工程)**:
|
||
|
||
1. **不要在前端 console / log / 錯誤訊息印出含 token 的完整 URL**(前端錯誤回報只記 modelId / status code,不記 download_url)。
|
||
2. **沿用短 TTL**(維持 §10.3 的 120s,Q2 已定 60–300s)。
|
||
3. (已天然滿足)opaque token + MC boundary + revoke 能力,無需額外開發。
|
||
4. **不需要**做更重的緩解(如 one-time token / IP 綁定)——與 FAA 設計範圍不符、第一階段過度工程。
|
||
|
||
### 11.7 決策 E:CORS 殘留確認 — **整條鏈乾淨**
|
||
|
||
| 跳 | origin 關係 | CORS 風險 |
|
||
|----|-----------|-----------|
|
||
| 前端 → visionA backend `GET /api/models/:id/download` | **同 origin**(9527→9527、走 `api.get`、帶 cookie) | 無 |
|
||
| 前端 → FAA `GET /files/...?access_token=`(瀏覽器導航) | 跨 origin(9527→5081),但**是導航、非 fetch、無自訂 header** | **無 preflight** → 無 CORS |
|
||
|
||
→ 改 redirect 後,**整條下載鏈不再有任何 CORS preflight**。FAA 不需設 CORS(對齊 FAA team 立場)。徹底解決 405。
|
||
|
||
### 11.8 落地 checklist(工程師照表施工)
|
||
|
||
**Backend(`visionA-backend`)— 採乙案:**
|
||
|
||
- [ ] `internal/api/models.go` `modelsDownloadHandler`(~`:550`):`downloadURL` 尾巴 append `?access_token=` + `url.QueryEscape(issued.Token)`。即 `downloadURL = {FAABaseURL}/files/{escapeFAAObjectKey(key)}?access_token={url.QueryEscape(token)}`。
|
||
- [ ] `ModelDownloadResponse`(`:457`):`Token` 欄位註解標 **deprecated**(仍回原始值、向下相容、前端不再使用);`DownloadURL` 註解更新為「已含 `?access_token=`、可直接瀏覽器導航下載」;`ExpiresAt` 保留。
|
||
- [ ] **不刪欄位**(向下相容)。
|
||
|
||
**Frontend(`visionA-frontend`):**
|
||
|
||
- [ ] `src/lib/api/model-download.ts`:
|
||
- [ ] **移除** `downloadModelFile`(fetch + Bearer + blob 那套)。
|
||
- [ ] 新增(或改造 `triggerBlobDownload`)成 `triggerNavDownload(downloadUrl: string)`:動態建 `<a href={downloadUrl} download rel="noopener">` + append + click + remove(**不需 createObjectURL / revokeObjectURL**)。
|
||
- [ ] `getModelDownload`:保留(仍打同 origin backend)。回傳型別 `ModelDownloadGrant` 的 `token` 改標 deprecated / optional(前端不再用)。
|
||
- [ ] **移除** `deriveDownloadFilename`(redirect 跨 origin 下檔名由 FAA `Content-Disposition` 決定、前端命名無效;見 §11.4)。
|
||
- [ ] 錯誤分層:導航式下載**無法攔截 FAA 端 4xx/5xx**(瀏覽器導航不回 Response 給 JS)。故 `network_error` / `download_failed` 這類「FAA 回應錯誤」前端**偵測不到**——錯誤處理範圍縮小為「getModelDownload 階段的錯誤」(backend 4xx/5xx)。更新註解說明此限制。
|
||
- [ ] `src/stores/model-store.ts` `downloadModel` action(~`:278`):
|
||
- [ ] 改為 `getModelDownload(model.id)` → `triggerNavDownload(grant.downloadUrl)`(拿掉 `deriveDownloadFilename` + `downloadModelFile`)。
|
||
- [ ] `downloadingId` loading 狀態:導航觸發是同步瞬間完成(無 await blob),loading 幾乎不可見——可保留作防重複點擊、但要意識到「下載進度」前端無法追蹤(瀏覽器接手)。
|
||
- [ ] `src/components/models/model-card.tsx`:下載按鈕邏輯大致不變(仍呼叫 `downloadModel`);但 toast「下載開始」語意仍成立(導航觸發即視為開始)。
|
||
|
||
**Tests:**
|
||
|
||
- [ ] `visionA-frontend/src/lib/api/model-download.test.ts`:
|
||
- [ ] **刪除** `downloadModelFile` 整個 describe(fetch happy / 非2xx / CORS network_error / abort / 空token)——該函式移除。
|
||
- [ ] **刪除** `deriveDownloadFilename` 整個 describe——函式移除。
|
||
- [ ] `triggerBlobDownload` describe → 改為 `triggerNavDownload`:驗「建立 anchor、href = 傳入的 downloadUrl、有 download 屬性、click 後從 DOM 移除」(**不再驗 createObjectURL / revokeObjectURL**)。
|
||
- [ ] `getModelDownload` describe:保留(仍驗 200 正規化 / 4xx / 5xx / parse)。
|
||
- [ ] `visionA-frontend/src/stores/model-store.test.ts`:`downloadModel` 相關 case 改為驗「呼叫 getModelDownload 後觸發導航(spy triggerNavDownload / spy anchor click)」,而非「呼叫 downloadModelFile」。
|
||
- [ ] `visionA-frontend/src/components/models/model-card.test.tsx`:互動 test 大致不變(mock `downloadModel` 回傳),確認按鈕點擊 → toast 行為。
|
||
- [ ] `visionA-backend/internal/api/models_download_test.go` `TestModelsDownload_OK`:`download_url` 斷言改為**含 `?access_token=<url.QueryEscape(fdt token)>`**(如 `.../files/models/demo-user/job-1.nef?access_token=fdt_abc123`)。其餘 case(501/404/403/502)不受影響。
|
||
|
||
**驗收(給 testing):**
|
||
|
||
- [ ] 後端:`download_url` 結尾含 `?access_token=` + token 正確 escape。
|
||
- [ ] 前端:點下載 → 建立帶 FAA URL 的 `<a>` 並 click(無 fetch 呼叫、無 CORS preflight)。
|
||
- [ ] e2e(stage):實際點下載按鈕、瀏覽器發起對 FAA 的導航 GET、檔案落地(檔名來自 FAA `Content-Disposition`)。
|
||
|
||
### 11.9 後果
|
||
|
||
**正面**:徹底解決 405 / CORS(FAA 不需設 CORS、對齊 FAA team 立場 + FAA 官方用法);前端大幅簡化(拿掉 fetch+blob+createObjectURL+檔名推導);token escape / URL 組裝收口到後端單一真實來源。
|
||
|
||
**負面(接受的取捨)**:(1) token 落瀏覽器歷史 / FAA log(§11.6 評估可接受);(2) 前端**無法攔截 FAA 端下載錯誤**(導航式下載不回 Response 給 JS)——FAA 4xx/5xx 時使用者看到的是瀏覽器原生錯誤頁 / 空白,而非 visionA 的 toast。第一階段接受(短 TTL + owner-only 已驗權限,FAA 端錯誤機率低);若未來需要前端可控的錯誤體驗,再評估「backend proxy 下載」(決策 2 §4 替代方案 A)。(3) `token` / `deriveDownloadFilename` 留為 deprecated 死碼(建議下次清理)。
|