visionA/docs/autoflow/04-architecture/adr/adr-015-server-to-server-api-key.md
jim800121chen dab13ed984 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>
2026-05-16 12:30:46 +08:00

629 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-015visionA → converter 採 pre-shared API key 認證(取代 OAuth client_credentials— 範圍縮限至 visionA → converter
## 狀態
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 §2MC 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 模式」中 **converter 部分**visionA → converter 的 service token / scope `converter:job.write/read` 取消、§7「失敗模式 retry 矩陣」中 converter MC token rowconverter 線不再經 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)
### Phase 0.8 OAuth client_credentials 鏈路失敗事件2026-05-09 ~ 11
ADR-014 原本設計 visionA backend → converter / FAA 的 server-to-server 認證走 Member CenterMC的 OAuth `client_credentials` grant
1. visionA backend 啟動時讀 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `SECRET`
2. 第一次需要時打 MC `POST /oauth/token` 換 service tokenscope `converter:job.write/read``files:download.read/delegate`
3.`Authorization: Bearer <service-token>` 打 converter / FAA
4. converter / FAA 端 middleware 驗 JWKS 簽章 + 驗 scope + 驗 tenant
Phase 0.8 stage 部署實際跑到才發現**整條鏈路有 4 個串行 blocker**
| # | Blocker | 影響 |
|---|---------|------|
| 1 | MC stage 沒註冊 `converter:job.read/write` 兩個 scopeprogress.md 5/2 `assume 都有` 證實是錯的)| `POST /oauth/token` 直接 400 `invalid_scope` |
| 2 | stage 上 converter image 是 5 週前舊版,沒 OAuth middleware、沒 `/api/v1/jobs` endpoint | 即使 MC 補 scopeconverter 仍無法接受 service token |
| 3 | converter 缺 `MEMBER_CENTER_*` env 設定 | converter 無法 init OIDC middleware |
| 4 | FAA stage 也可能要 OAuth 整合warrenchen 維護)| 不確定狀態,需跨人協調 |
要修齊這 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** 的場景**完全不符合上面任一個**
- **1:1 trust 關係**visionA 是 converter 唯一的 server-to-server caller沒有第三方
- **使用者同時維護 visionA + converter**jimchen可單方拍板改 middleware
- **全部 internal trust**(不是給外部 dev 用,沒有 untrusted client
- **無需 scope 細分**converter 只關心「是否為 visionA」單一布林
而成本:
- **複雜度**MC 端要 onboard scope、issuer JWKS 要可達、token cache 要寫對、TTL 邊界要處理、4 個 5xx / 4xx 失敗模式要 retry & graceful degrade
- **鏈路長度**visionA → MC → cache → converter任一節點掛掉都不能轉檔
- **可觀測性負擔**:要追的 metrics 包含 token cache hit rate、MC 失敗率、scope 對齊狀態
### 為什麼 v2.0 撤回 FAA 改 API keyFAA 線適用相反邏輯)
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:NFAA 之後可能服務多個 visionA-like 產品線),維持 OAuth 框架對 FAA 端架構演進更友善
→ v2.0 縮限至「**只動 converter 線FAA 線回到 ADR-014 §2 原設計**」是更精確的責任邊界劃分。
### 已洩漏的 stage service client secretv1.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)
### 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 → converterv1.0 採用 / v2.0 維持)
```
visionA backend 啟動時
讀 env VISIONA_CONVERTER_API_KEY
[轉檔請求進來]
打 converter
Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
```
converter 端 middleware
```
讀 env CONVERTER_API_KEY
[收到請求]
parse Authorization header → 取 token
subtle.ConstantTimeCompare(token, CONVERTER_API_KEY)
match → 放行mismatch → 401
```
### 2. ⚠️ visionA → FAAv2.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` endpointvisionA 無法跟 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 啟動時
讀 OIDCConfig.ServiceClientID / ServiceClientSecret + ConversionConfig.TenantID
[需要打 FAA例如「加到模型庫」server-to-server pull、或「下載」proxy]
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_tokencache 至 exp - 15s
A 路線FAA 寫 / metadata / delete / s2s download
visionA 帶 Authorization: Bearer <service-token> 打 FAA
FAA 端 .RequireAuthorization() + EnsureJwtScopeAndTenant 驗
├─ JWT 簽章FAA `AddJwtBearer` Authority = MC issuer自動 JWKS
├─ AudienceFAA `Auth:Audience`
├─ scope claimPUT 要 files:upload.writeGET metadata / HEAD 要 files:metadata.readDELETE 要 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
```
**為什麼 v2.0 設計「download 線必須是 delegated token」而非「service token」**
FAA 端的 dual-auth 設計(已實作於 `/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs`)強制如此:
| 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 tokenscope `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. 單一下游converterv1.x「每個下游各自獨立的 key」表格 v2.0 縮限)
**v1.x 的 key 表格** 縮限至 converter 一條:
| Key | 持有者 | 用途 |
|-----|--------|------|
| `VISIONA_CONVERTER_API_KEY`visionA 端) / `CONVERTER_API_KEY`converter 端) | jimchen | visionA → converter |
**v1.x 中的 FAA key row`VISIONA_FAA_API_KEY` / `FAA_API_KEY`)撤回**——FAA 改回 MC service token 路徑、不需要 pre-shared API key。
理由converter 維持):每條 trust boundary 各自獨立。converter 線 1:1 trust雙方都由 jimchen 維護rotate 對齊成本可控。
### 3.5 Reference Middleware Implementationv2.0 縮限至 converter 端)
本節提供 converter 端 middleware 的可直接照抄 reference snippet。
#### 3.5.1 converter 端Go — net/http 標準 middleware pattern
`net/http` 標準 middleware pattern可直接套用 `chi` / `gorilla/mux` / 原生 `http.ServeMux``subtle.ConstantTimeCompare` 是 Go 標準庫提供的 constant-time 比較函式(避免 timing attack
```go
// internal/middleware/apikey.go
package middleware
import (
"crypto/subtle"
"errors"
"log/slog"
"net/http"
"strings"
)
// ErrAPIKeyNotConfigured 啟動時 server 端 API key 未設定 — 應在 main() init 時 fail-fast、
// 不要等到第一個 request 才發現
var ErrAPIKeyNotConfigured = errors.New("CONVERTER_API_KEY env not set")
// NewAPIKeyAuth 回傳一個驗證 Authorization: Bearer <api-key> 的 middleware。
// 若 expectedKey 為空字串,會直接 panic啟動 fail-fast— 避免「未設定 = 全部放行」的災難。
func NewAPIKeyAuth(expectedKey string, logger *slog.Logger) func(http.Handler) http.Handler {
if expectedKey == "" {
// 啟動時偵測 — 不允許 server 在沒有 key 的狀態下啟動
panic(ErrAPIKeyNotConfigured)
}
expectedBytes := []byte(expectedKey)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
// missing Authorization header
if authHeader == "" {
logger.Warn("api key auth failed",
"reason", "missing_authorization_header",
"path", r.URL.Path,
"remote", r.RemoteAddr)
writeUnauthorized(w)
return
}
// 不是 Bearer prefix必須有 "Bearer " 前綴 + 空格)
const prefix = "Bearer "
if !strings.HasPrefix(authHeader, prefix) {
logger.Warn("api key auth failed",
"reason", "missing_bearer_prefix",
"path", r.URL.Path,
"remote", r.RemoteAddr)
writeUnauthorized(w)
return
}
token := strings.TrimSpace(authHeader[len(prefix):])
// token 為空("Bearer " 後面什麼都沒有)
if token == "" {
logger.Warn("api key auth failed",
"reason", "empty_token",
"path", r.URL.Path,
"remote", r.RemoteAddr)
writeUnauthorized(w)
return
}
// constant-time compare — 即使長度不同 ConstantTimeCompare 也會回 0但為了
// 提早 short-circuit、先檢查長度可避免 hash 不必要的工作(長度本身不是 secret
if subtle.ConstantTimeCompare([]byte(token), expectedBytes) != 1 {
logger.Warn("api key auth failed",
"reason", "token_mismatch",
"path", r.URL.Path,
"remote", r.RemoteAddr)
// 注意log 絕對不印 token 本身
writeUnauthorized(w)
return
}
// 通過 — 放行到 next handler
next.ServeHTTP(w, r)
})
}
}
// writeUnauthorized 統一回 401 — 不洩漏「key 對 / 不對」/ 「missing / mismatch」的差異
func writeUnauthorized(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
}
```
main 端使用範例:
```go
// cmd/converter/main.go節錄
expectedKey := os.Getenv("CONVERTER_API_KEY")
authMiddleware := middleware.NewAPIKeyAuth(expectedKey, logger)
// 套用到所有 /api/v1/* routes
mux.Handle("/api/v1/", authMiddleware(apiHandler))
```
#### 3.5.2 ~~FAA 端C# ASP.NET Core middleware~~v2.0 撤回 — 整段刪除)
> **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 tokenFAA 端**不需要新增任何 API key middleware**
>
> - 既有 `AddJwtBearer` + `RequireAuthorization()` + `EnsureJwtScopeAndTenant` 已涵蓋 PUT / metadata / HEAD / DELETE 4 個 endpoint
> - 既有 `IDelegatedDownloadTokenValidator` 已涵蓋 GET download endpoint
> - **FAA 端零變更**——這是 v2.0 撤回的核心收益(不必動公司共用 FAA repo、不必跟 warrenchen 協調)
#### 3.5.3 部署檢查清單v2.0 縮限至 converter 端)
不分 client 端 / server 端,部署前 converter 兩側必須逐項確認:
| # | 檢查項 | 為什麼 |
|---|--------|--------|
| 1 | env 已設定且非空(啟動 fail-fast| 避免「未設定 = 全部放行」災難server 應在啟動時 panic / throw、不要等到第一個 request 才發現 |
| 2 | constant-time compareGo `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 |
| 6 | 每環境dev / stage / prod獨立 key | 嚴格分環境產 key`openssl rand -hex 32` 各 64 字元 hex不重用 |
| 7 | key 不進 git | `.gitignore` 嚴格 ignore `.env*`CI / CD secret 從 Secrets Manager / Vault 注入 |
| 8 | 健康檢查 endpoint 是否要 bypass auth | 通常 `/healthz` / `/readyz` 應 bypass讓 LB / k8s 可探測),但業務 endpoint 全部要 auth |
> **FAA 端不需要本清單**——v2.0 起 FAA 端使用既有 OAuth + delegated token 機制,無新增 API key middleware。FAA 端的 OAuth / delegated token 部署檢查請參考 ADR-014 §2 與 `conversion.md` §3.2。
### 4. 不再有 scope 概念converter 線適用FAA 線 v2.0 撤回)
**converter 線**OAuth `client_credentials` 設計中兩個 converter scope`converter:job.write/read`)取消。
- 單一 API key 就是「visionA 有權打 converter」的完整證明
- 不再有「同一個 client 但拿不同 scope」的細粒度區分在 1:1 trust 中本來就沒意義)
- converter 端 middleware 也不需要驗 scope
**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` 驗。
### 5. tenant 概念
**converter 線**visionA → converter 不再帶 tenant_id。converter 端的 user_id 從 multipart body `user_id` field 拿(仍由 visionA 從 OIDC sub 灌入,這條 trust boundary 不變。converter 端不需要 tenant 概念。
**FAA 線v2.0 修正 — 從 v1.x「不再有 tenant」改為「FAA 線需要 tenant」**
- 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...` 註冊時對應的 tenantstage 為 `732270c0-449c-489c-bfad-321e9bf89b3d`
-`VISIONA_OIDC_TENANT_ID` env **重新啟用**v1.x 標廢棄v2.0 撤回廢棄)
### 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 // v1.0 新增 — env VISIONA_CONVERTER_API_KEYv2.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 改判定:
func (c ConversionConfig) Enabled() bool {
return c.ConverterBaseURL != "" &&
c.FAABaseURL != "" &&
c.ConverterAPIKey != "" &&
// FAA 線判定 OIDCConfig 有 ServiceClientID / Secret + ConversionConfig.TenantID
// — 由 main.go 啟動時組合判斷,這裡只判 conversion 自己的欄位
c.TenantID != ""
}
```
### 7. Delegated download token 路徑的處理v2.0 撤回 v1.x 的撤回,回到 ADR-014 §2 設計)
ADR-014 §2 原設計 download 流程:
```
browser → visionA /download → MC issue delegated token → 302 → browser → FAA?access_token=...
```
**v1.x 的選項 A短期 server-side proxy with API key撤回**v1.0 / v1.1 在本節原本提案「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
ownership 檢查
ensurePromoted拿 target_object_key
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
```
**為什麼保留 server-side stream proxy不退回 ADR-014 §2 的 302 redirect**
- 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 tokenline 184-254 沒掛 `RequireAuthorization()`、改用 `IDelegatedDownloadTokenValidator`FAA 端不接受其他 token type
- v1.x 若要用 visionA API key需要 warrenchen 在 FAA 端為 download endpoint 額外實作「API key middleware」並改寫 dual-auth 路由——成本遠超 v2.0 撤回的收益
**選項 BPhase 1+ HMAC token 升級路徑)保留為 follow-up**
如果 Phase 1 流量壓力大要回 302 redirect 模式visionA 可以自己簽 short-TTL HMAC token不需要 MC 介入FAA 端 middleware 多加一條「驗 visionA HMAC token」的路徑
```
browser → visionA /download → visionA 用 HMAC_KEY 簽 short-TTL token
302 → browser
browser → FAA?access_token=<visionA-signed-hmac>
FAA middlewareJWT (s2s) OR delegated (current) OR HMAC (browser direct) 三選一
```
此升級路徑與本 ADR v2.0 決策無衝突,記入 Phase 1 follow-up同 v1.x 規劃)。
### 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` fieldconverter 端視 visionA 為 trusted caller
- API keyconverter/ service tokenFAA證明的是「caller 是 visionA」user_id 的真實性由 visionA 內部的 OIDC 機制保證 — 兩條獨立鏈
### 9. 部署層的 env 注入v2.1 修訂)
Phase 0.8b v2.1 採用visionA 端 server-to-server secret 只剩 converter API key 一把):
| 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 產生方式:
- converter API key兩端對齊`openssl rand -hex 32`64 字元 hex
- ~~service client secret~~v2.1 不需要)
**v2.1 後 visionA 端 server-to-server 鏈路收斂為單條**visionA → converterAPI keydownload 也走同一條converter `GET /api/v1/jobs/{id}/result`,詳見 ADR-016
## 考慮過的替代方案 (Alternatives Considered)
### 方案 A維持 OAuth client_credentialsADR-014 原方案 — converter / FAA 兩條都走)
| 評估 | 內容 |
|------|------|
| 優點 | 標準化、跨團隊可重用、短 TTL token、scope 細粒度可控 |
| 缺點 | 需要 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 不好 |
### 方案 BmTLSmutual TLS
| 評估 | 內容 |
|------|------|
| 優點 | 不需傳遞 secret in plaintext憑證綁定、cert rotation 機制成熟 |
| 缺點 | converter / FAA 都要支援 mTLS、需要 CA 管理、ingressnginx / Caddy / ALB也要配合 client cert termination、stage 環境部署成本高 |
| 排除原因 | 對 1:1 trust 過度設計;公司 stage 環境 ingresshost nginx未對外開放 mTLS 配置;維運成本不成比例 |
### 方案 CAPI key + IP allowlist 雙層防護
| 評估 | 內容 |
|------|------|
| 優點 | 即使 API key 洩漏,攻擊者也需從特定 IP 才能用 |
| 缺點 | visionA 上 AWS 後 IP 不固定NAT gateway / ALB 對外多 IPconverter / FAA 在公司內網 IP allowlist 維護成本高;對「不是內網」的 prod 場景幾乎沒用 |
| 排除原因 | Phase 0.8 stage 仍在公司內網visionA stage 在 192.168.0.x加 IP allowlist 在 stage 可行但對 prod 沒有延展性;不採用 |
### 方案 D共用一把 API key不分 converter / FAA
| 評估 | 內容 |
|------|------|
| 優點 | env 少一個、部署設定簡單 |
| 缺點 | 一處洩漏兩處連坐converter rotate 必須同步 FAA違反「每條 trust boundary 各自獨立」原則 |
| 排除原因 | 在 v1.x 兩條都 API key 的設計中作為反方案被排除v2.0 因 FAA 撤回 API key、議題自然不存在 |
### 方案 Ev2.0 採用visionA → converter API key、visionA → FAA 仍走 MC service token + delegated download token
| 評估 | 內容 |
|------|------|
| 優點 | (1) 不必動 FAA repowarrenchen 維護成本歸零);(2) 不必動 MC 端 scope onboarding4242ba63 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) 不希望動 MC5/9 撞 scope 沒註冊的痛);(3) 但 5/16 提供的 service client `4242ba63...` 證明 MC 端針對 FAA 的 4 個 scope 已備妥 — v1.0 拒絕走 OAuth 的「MC scope 沒備妥」前提在 FAA 線**不成立**|
| 採用 | **v2.0 採用** |
## 後果 (Consequences)
### 正面影響
- **converter 線實作大幅簡化**v1.0 收益保留visionA backend 對 converter 的呼叫不查 cache、不打 MC、不重簽
- **converter / FAA stage e2e blocker 收斂**converter 線 0 個 blockerFAA 線靠使用者已備妥的 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...` 取代
### 負面影響(接受的取捨)
- **converter 線 API key 是 long-lived secret**:不像 OAuth token 有 TTL通常 1 小時rotate 需要 visionA + converter 同步換 env 並 redeploysecret 管理責任更重
- **converter 線沒有 scope 細粒度**:未來如果 visionA 內部要區分「能 init job 但不能 promote」這種需求要回頭加但 1:1 trust 場景幾乎不會有此需求)
- **converter 線沒有 audit trail誰用 token 做什麼)**OAuth + JWT 的 sub claim 提供天然 auditAPI key 模式下 converter 只知道「是 visionA」需要靠 visionA 內部 log + request_id 串接才能追到 user_id既有 request_id 機制可滿足)
- **FAA 線維持 OAuth client_credentials 鏈條的部分複雜度**v2.0 新增visionA 仍需 mc_token_clientservice token cache + delegated download token issue + retry policyMC 仍是 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 過期)
### 風險
| 風險 | 緩解 |
|------|------|
| 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 runbookjimchen 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 | 嚴格分環境產 keydev / stage / prod 各自 `openssl rand -hex 32`不重用 |
## 合規性
- [x] 與使用者確認v2.0 範圍縮限至 visionA converter API keyvisionA 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範圍縮限至 converter API key
- [ ] 已洩漏的 stage service client secret `RciRUyi...` 自動作廢不需 MC rotatev2.0 由新 client `4242ba63...` 取代
## 配套產出(給後續 Phase
### Phase 0.8b 範圍內v2.0 修訂)
- 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
- [ ] converter API key rotate runbook產新 key 雙方同步 env restart 驗證 拔舊 key步驟範圍縮限至 converter
- [ ] 是否設 converter API key 有效期例如 1 年到期自動提醒)— SRE 流程決定
- [ ] FAA visionA 自己簽 HMAC tokendelegated 機制(§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)**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 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-57JWT Bearer 設定)、line 184-254download endpoint 不掛 RequireAuthorization IDelegatedDownloadTokenValidator)、line 291-322EnsureJwtScopeAndTenant + HasScope
## 版本記錄
| 日期 | 版本 | 變更 |
|------|------|------|
| 2026-05-11 | 1.0 | 初版 Phase 0.8b server-to-server 認證從 OAuth client_credentials pre-shared API keyvisionA 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 tokenvisionA 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` endpointvisionA 無法跟 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 keyv1.0 / v2.0 / v2.1 都維持不變**。 |