Auth pillar 從 OAuth 2.0 resource server 改成 pre-shared API key (visionA ↔ converter 1:1 internal trust)。新增 GET /api/v1/jobs/:id/result streaming endpoint 給 visionA backend 中轉 NEF 下載。 Phase A(auth 切換): - 新增 apiKeyMiddleware(constant-time compare、tokenFingerprint、4 audit events) - 砍 OAuth middleware + JWKS(保留 oauthClient 供 promote → FAA 使用) - 4 個 endpoint 換掛 requireApiKey - 加 TRUST_PROXY env + Express trust proxy 設定(forensic source_ip) Phase B(/result endpoint): - streaming NEF download with 5min timeout + concurrent cap 10 - Two-tier rate limit(burst 5/10s + sustained 20/min) - Bandwidth quota(1 GB/hr + 6 GB/24hr)by token_fingerprint - Range header silently ignored + Accept-Ranges: none - filename quote-escape + RFC 5987 fallback + sanitize - 8 個 /result audit events(forensic 完整) 設計演進記錄:docs/TODO-visionA-integration-v2.md(5/2 OAuth → 5/16 API key → 5/16 download via converter;對應 visionA repo ADR-015/016) Tests: 597 → 666 (+69)、29 suites all pass Security: APPROVE WITH CONDITIONS(單 instance 部署、6 新 env、24hr 監控) npm audit: 3 vuln → 0(transitive AWS SDK xml chain) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
295 lines
13 KiB
Markdown
295 lines
13 KiB
Markdown
# TDD 索引 — Kneron Model Converter 對外 API
|
||
|
||
## 作者:Architect Agent
|
||
## 狀態:Draft(Phase 0.8b 重寫 + 模組化)
|
||
## 最後更新:2026-05-16
|
||
|
||
> **auth 設計演進**:本 TDD 反映 Phase 0.8b 拍板後的「目標狀態」。完整歷史見 visionA repo `docs/autoflow/04-architecture/adr/adr-015-server-to-server-api-key.md` v2.1 + `adr-016-download-via-converter.md` v1.0。
|
||
>
|
||
> **配套**:`design-doc.md`(架構決策)、`../02-prd/PRD.md`(需求)、`../03-design/design-review.md`(UX 回饋)。
|
||
|
||
---
|
||
|
||
## 變更歷程
|
||
|
||
| 日期 | 變更 | 作者 |
|
||
|------|------|------|
|
||
| 2026-04-25 | 初版 Draft 1.0(OAuth resource server + promote) | Architect Agent |
|
||
| 2026-04-25 | Multipart 上傳路徑改 visionA → converter 直傳;移除 FAA GET/HEAD | Architect Agent |
|
||
| 2026-05-16 | **Phase 0.8b 重寫**:visionA → converter 改 API key;新增 `/result` endpoint;OAuth resource server 章節砍除;模組化拆分為索引 + 子檔案 | Architect Agent |
|
||
|
||
---
|
||
|
||
## 1. 文件結構
|
||
|
||
本 TDD 在 Phase 0.8b 重寫時拆分為模組化結構:
|
||
|
||
| 檔案 | 內容 | 目標讀者 |
|
||
|------|------|---------|
|
||
| `TDD.md`(本檔,索引)| 各章節摘要 + 子檔案連結 | 全部 |
|
||
| `auth.md` | API key middleware 設計 + 砍除 OAuth resource server 清單 + 保留 OAuth client | Backend |
|
||
| `api/api-jobs.md` | `POST/GET /jobs`、`GET /jobs/:id` 規格 | Backend、Reviewer、Testing |
|
||
| `api/api-promote.md` | `POST /jobs/:id/promote` 規格 | Backend、Reviewer、Testing |
|
||
| `api/api-result.md` | **新增** `GET /jobs/:id/result` 規格 | Backend、Reviewer、Testing |
|
||
| `database.md` | Redis schema + 索引 + Lua script | Backend |
|
||
| `infra.md` | Nginx / docker-compose / .env 變動 + 部署順序 | Backend、DevOps |
|
||
| `performance.md` | SLO + 延遲預算 + 負載測試 | Backend、Testing |
|
||
| `observability.md` | Log 格式 + 敏感資料保護 + 告警 | Backend |
|
||
| `security.md` | Trust boundary + Input validation + Auth security | 全部 |
|
||
| `design-doc.md` | 架構決策 + ADR | 全部 |
|
||
|
||
---
|
||
|
||
## 2. Phase 0.8b 改動摘要
|
||
|
||
### 2.1 對外 auth 改 API key
|
||
|
||
- 砍 `auth/middleware.js`(OAuth resource server)+ `auth/jwks.js`
|
||
- 加 `auth/apiKeyMiddleware.js`
|
||
- 4 個既有 endpoint 改掛 `requireApiKey()`
|
||
- 新加 `/result` endpoint 也用 `requireApiKey()`
|
||
|
||
詳見 `auth.md` §1 + §3(砍除清單)。
|
||
|
||
### 2.2 新增 `/result` endpoint
|
||
|
||
- `GET /api/v1/jobs/:id/result`
|
||
- Streaming proxy NEF from MinIO → caller
|
||
- 4 種 4xx + 2 種 5xx 情境
|
||
- 雙路徑 NEF key 解析(新格式 + 舊格式向後相容)
|
||
- **2026-05-17 補充**:rate limit(60 req/min,獨立 bucket)、Range header 防護(silently ignore)、audit log 8 個 action、Backend `source_filename` 寫入 acceptance criteria(§9-§14)
|
||
|
||
詳見 `api/api-result.md`。
|
||
|
||
### 2.3 保留不動
|
||
|
||
- Promote 流程(converter → FAA 仍走 OAuth client_credentials)
|
||
- Redis schema(除確認 `source_filename` 欄位存在)
|
||
- Worker、MinIO bucket、Nginx 結構
|
||
|
||
詳見 `auth.md` §2 + `api/api-promote.md`。
|
||
|
||
### 2.4 Config 變動
|
||
|
||
- 移除:`MEMBER_CENTER_ISSUER` / `MEMBER_CENTER_JWKS_URL` / `KNERON_CONVERTER_AUDIENCE` / `JWKS_*` / `JWT_CLOCK_TOLERANCE_SEC`
|
||
- 新增:`CONVERTER_API_KEY`
|
||
- 保留:`MEMBER_CENTER_TOKEN_URL` / `KNERON_CONVERTER_CLIENT_*` / `FILE_ACCESS_AGENT_*` / `OAUTH_*`
|
||
|
||
詳見 `infra.md` §3。
|
||
|
||
---
|
||
|
||
## 3. 系統概述
|
||
|
||
### 3.1 角色
|
||
|
||
- **Converter(本專案)**:Node.js Task Scheduler + Python Worker
|
||
- **visionA-backend**:Go 服務,Converter 對外 API 的**唯一** caller
|
||
- **Member Center(MC)**:OAuth authorization server — Phase 0.8b 後**只**給 Converter → FAA promote 用
|
||
- **File Access Agent(FAA)**:NAS 邊界檔案閘道,single-tenant per instance
|
||
|
||
### 3.2 API 端點清單(Phase 0.8b 後)
|
||
|
||
| 方法 | 路徑 | Auth | 說明 | 規格 |
|
||
|------|------|------|------|------|
|
||
| GET | `/health` | — | 健康檢查 | `api/api-jobs.md` §3 |
|
||
| POST | `/api/v1/jobs` | API key | 建立 job | `api/api-jobs.md` §4 |
|
||
| GET | `/api/v1/jobs` | API key | 列表 / Recovery | `api/api-jobs.md` §6 |
|
||
| GET | `/api/v1/jobs/:id` | API key | 單一 job 狀態 | `api/api-jobs.md` §5 |
|
||
| POST | `/api/v1/jobs/:id/promote` | API key | 搬檔到 FAA | `api/api-promote.md` |
|
||
| GET | `/api/v1/jobs/:id/result` | API key | **NEW** stream NEF | `api/api-result.md` |
|
||
| POST | `/api/v1/jobs/:id/download-tokens` | API key | Phase 2,回 501 | `api/api-jobs.md` §7 |
|
||
| DELETE | `/api/v1/jobs/:id` | API key | Phase 2,回 501 | `api/api-jobs.md` §7 |
|
||
|
||
### 3.3 既有路徑(Phase 0.8b 不動)
|
||
|
||
| 方法 | 路徑 | 用途 |
|
||
|------|------|------|
|
||
| POST | `/jobs` (multipart) | Web UI 既有上傳 |
|
||
| GET | `/jobs/:id` | Web UI 狀態查詢 |
|
||
| GET | `/jobs/:id/events` (SSE) | Web UI 進度 push |
|
||
| GET | `/jobs/:id/download/:filename` | Web UI 下載 |
|
||
| GET | `/queues/stats` | 內部監控 |
|
||
|
||
這些走 internal vhost,不對外、不加 auth。
|
||
|
||
---
|
||
|
||
## 4. 技術堆疊(不變)
|
||
|
||
| 層級 | 選擇 |
|
||
|------|------|
|
||
| 後端框架 | Node.js 18 + Express 4 |
|
||
| 認證(對外)| API key(`crypto.timingSafeEqual`,Phase 0.8b 新)|
|
||
| 認證(promote)| OAuth client_credentials(jose / 自寫 fetch)|
|
||
| 資料庫 | Redis 7 |
|
||
| 物件儲存 | MinIO(Converter Bucket) |
|
||
| Worker | Python 3.10+ |
|
||
| 反向代理 | Nginx |
|
||
| 測試 | Jest |
|
||
|
||
詳見 `design-doc.md` §3.5。
|
||
|
||
---
|
||
|
||
## 5. 專案結構
|
||
|
||
```
|
||
apps/task-scheduler/
|
||
├── server.js ← Entry
|
||
├── src/
|
||
│ ├── config.js ← 集中讀 env(Phase 0.8b 改)
|
||
│ ├── redis.js ← Redis client
|
||
│ ├── auth/
|
||
│ │ ├── apiKeyMiddleware.js ← 【新】Phase 0.8b
|
||
│ │ ├── oauthClient.js ← 【保留】promote 用
|
||
│ │ ├── middleware.js ← 【砍】OAuth resource server
|
||
│ │ └── jwks.js ← 【砍】
|
||
│ ├── fileAccessAgent/
|
||
│ │ ├── client.js ← FAA HTTP client(保留)
|
||
│ │ └── errors.js ← 錯誤翻譯(保留)
|
||
│ ├── routes/
|
||
│ │ ├── legacy.js ← 既有 /jobs/* 路由
|
||
│ │ └── v1/
|
||
│ │ ├── index.js ← v1 router 組裝(要改 wire result + 換 auth middleware)
|
||
│ │ ├── jobs.js ← POST/GET(要改換 requireApiKey)
|
||
│ │ ├── promote.js ← POST promote(要改換 requireApiKey)
|
||
│ │ └── result.js ← 【新】Phase 0.8b
|
||
│ ├── services/
|
||
│ │ ├── jobService.js ← Job CRUD
|
||
│ │ └── doneListener.js ← Worker done event
|
||
│ ├── middleware/
|
||
│ │ ├── errorHandler.js ← 統一錯誤
|
||
│ │ └── requestId.js
|
||
│ └── utils/
|
||
│ └── logger.js
|
||
├── docs/openapi.yaml ← 要改 security scheme(OAuth → bearer/api_key)
|
||
├── .env.example ← 要改(見 infra.md §4)
|
||
├── README.md ← 要改 auth 章節
|
||
└── package.json
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 實作任務拆分(給 Backend)
|
||
|
||
按 Autoflow 增量式開發規範,每個任務 = 一個可獨立 review 的單位。
|
||
|
||
### Phase A — API key middleware + auth 切換(取代 OAuth)
|
||
|
||
| # | 任務 | 依賴 | 預估 | 驗收標準 |
|
||
|---|------|------|------|---------|
|
||
| A1 | 新建 `src/auth/apiKeyMiddleware.js` | — | 1d | unit test 全過:happy path、missing header、wrong key、constant-time、destroy socket、env 未設定 fail-fast |
|
||
| A2 | 修 `src/config.js`:新增 `converter.apiKey`、移除 OAuth resource server 相關 env、保留 promote 相關 | — | 0.5d | config.test.js 過;啟動時 `CONVERTER_API_KEY` 未設只 warn(不 throw);OAuth resource server env 移除後 server 仍能啟動 |
|
||
| A3 | 修 `src/routes/v1/index.js` / `jobs.js` / `promote.js`:`requireAuth(scope)` → `requireApiKey()` | A1, A2 | 0.5d | 既有 integration test 全過(401 行為改成 API key 模式驗);server 啟動正常 |
|
||
| A4 | 砍 `src/auth/middleware.js` + `src/auth/jwks.js` + 相關 test | A3 | 0.5d | `git rm` + test runner 沒 broken import;search code base 沒有 reference 殘留 |
|
||
| A5 | 修 `.env.example`、`docs/openapi.yaml`、`README.md`:移 OAuth resource server 段、加 `CONVERTER_API_KEY` | A4 | 0.5d | docs lint 過;OpenAPI security scheme 改 bearer / api_key |
|
||
| A6 | Integration test:API key 驗證 4 個情境(happy / missing / wrong / 503) | A1-A5 | 1d | 全部過;既有 jobs / promote integration test 仍過 |
|
||
|
||
**Phase A 總工時**:~4d
|
||
|
||
### Phase B — `/result` endpoint
|
||
|
||
| # | 任務 | 依賴 | 預估 | 驗收標準 |
|
||
|---|------|------|------|---------|
|
||
| B1 | 確認 `jobService.createJob` 寫入 `source_filename` 欄位(檢查既有 code、補上若缺)| — | 0.5d | unit test 過;既有 job record 結構不破壞 |
|
||
| B2 | 新建 `src/routes/v1/result.js`(含 `extractNefObjectKey`、`buildFilename`、stream handler)| B1, A1 | 1.5d | unit test 過:filename 各情境、雙路徑 key 解析、stream error / client close handling |
|
||
| B3 | Wire `/result` 到 `src/routes/v1/index.js`(含 `requireApiKey` + per-client rate limiter)| B2 | 0.5d | server 啟動 + route table 正確;mergeParams 取 :id 通 |
|
||
| B4 | Integration test:`/result` 8 個情境(200 happy / 401 / 404 job / 404 result / 409 / 410 expired / 410 minio miss / 502)| B2, B3 | 1d | 全部過 |
|
||
|
||
**Phase B 總工時**:~3.5d
|
||
|
||
### 任務排程建議
|
||
|
||
**順序執行 A → B**(Backend 單人):
|
||
|
||
- A1 + A2 可平行
|
||
- A3 等 A1 + A2
|
||
- A4 等 A3
|
||
- A5 等 A4
|
||
- A6 等 A5(整體 verify)
|
||
- B1 + B2 可平行(B1 簡單,B2 是主要工作)
|
||
- B3 等 B2
|
||
- B4 等 B3
|
||
|
||
預估總工時:~7.5 工作日(單人)。若可雙人並行,A 和 B 可分工,壓到 ~5d。
|
||
|
||
### 與 visionA 端的 dependency
|
||
|
||
| Backend 任務狀態 | visionA 端可以做什麼 |
|
||
|---------------|------------------|
|
||
| Phase A 完成、deploy stage | visionA 可以打 stage converter 的既有 endpoint 驗 API key 流程 |
|
||
| Phase B 完成、deploy stage | visionA 可以打 `/result` endpoint 驗 streaming |
|
||
| Phase A + B 都 deploy 完 | e2e 驗證(visionA repo commit 9e29ebf 已 ready) |
|
||
|
||
---
|
||
|
||
## 7. 測試策略
|
||
|
||
詳見 `performance.md` §7 + 各 `api/*.md` 的 test 章節。
|
||
|
||
### 7.1 Unit test 覆蓋率目標
|
||
|
||
- `apiKeyMiddleware`:100%(少量 code、必須全 cover)
|
||
- `result.js`:90%
|
||
- 既有 OAuth-related 改動:維持 ≥ 85%
|
||
|
||
### 7.2 Integration test 必跑
|
||
|
||
- API key 4 情境(happy / missing / wrong / 503)
|
||
- 既有 jobs / promote 在 API key 模式下仍過
|
||
- `/result` 8 情境(見 `api/api-result.md` §7.1)
|
||
|
||
### 7.3 Manual stage e2e(部署後)
|
||
|
||
- curl 驗:`/health`、`POST /jobs`、`GET /jobs/:id`、`POST /promote`、`GET /result`
|
||
- visionA 端 e2e:完整 upload → poll → promote → download
|
||
|
||
---
|
||
|
||
## 8. 安全注意事項
|
||
|
||
詳見 `security.md`。重點:
|
||
|
||
- `CONVERTER_API_KEY` 不進 git / log / Slack
|
||
- `constant-time compare`(防 timing attack)
|
||
- Sec C1 暫緩(`.env` history rewrite + secret rotation 在 Phase 1 ready 後做、含 CONVERTER_API_KEY)
|
||
- Trust boundary:visionA 一旦被 compromise 可冒充任意 user_id(接受、與 OAuth 模型一致)
|
||
|
||
---
|
||
|
||
## 9. 風險與待確認
|
||
|
||
| # | 風險 | 影響 | 行動 |
|
||
|---|------|------|------|
|
||
| R1 | CONVERTER_API_KEY rotation 流程未自動化 | 低 | Phase 1 接受手動 |
|
||
| R2 | `/result` 高並發 stream 壓力 | 低 | NEF 通常小、visionA 是唯一 caller、QPS 可控 |
|
||
| R3 | Sec C1 暫緩(.env 進 git history) | 中 | Phase 1 ready 收尾後 rewrite |
|
||
| R4 | NEF 7 天過期後 client 重新轉檔 | 低 | API spec 已定義 410,visionA 端處理 |
|
||
| R5 | Phase 0.8b 部署期間「OAuth → API key」短暫不可用 | 低 | 既有 stage OAuth 從未跑通、不會有 regression |
|
||
|
||
---
|
||
|
||
## 10. 後續步驟
|
||
|
||
1. 本 TDD 索引 + 子檔案送 PM / Design 三方互審
|
||
2. 使用者審核
|
||
3. Backend Agent 依 §6 的任務拆分增量開發
|
||
4. Reviewer 每個任務把關
|
||
5. Testing 整合測試 + e2e
|
||
6. DevOps 部署(converter 先 + visionA 後)
|
||
|
||
---
|
||
|
||
## 11. 變更記錄
|
||
|
||
| 日期 | 版本 | 變更 | 作者 |
|
||
|------|------|------|------|
|
||
| 2026-04-25 | Draft 1.0 | 初版,Phase 1 完整規格(單檔 1390 行) | Architect Agent |
|
||
| 2026-04-25 | Draft 1.1 | Multipart 上傳路徑改 | Architect Agent |
|
||
| 2026-05-16 | Draft 2.0 | **Phase 0.8b 重寫**:API key + /result + 模組化拆分為索引 + 8 個子檔案 | Architect Agent |
|
||
|
||
---
|
||
|
||
**附註**:本 TDD 從 1390 行單檔重組為 ~180 行索引 + 8 個子檔案。每個子檔案 < 500 行(單一職責),可獨立給 Backend / Reviewer / Testing 不同角色讀對應檔案、減少 context 負擔。
|