jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

513 lines
22 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.

# Security — 安全考量與 Pairing Token 協定
> 本文件彙整所有安全相關細節。部分對應 Design Doc §6。
---
## 1. Pairing Token 協定 {#pairing-token}
### 1.1 雛形版v0.1
最簡化單一 token
```bash
[開發者 setup]
# 產生符合正式格式的雛形 tokenvAc_ + 32 hex
export VISIONA_PAIRING_TOKEN="vAc_$(openssl rand -hex 16)"
export VISIONA_STATIC_USER_ID=demo-user
[local agent]
啟動時讀同一個 env或 config 檔 / CLI flag
連 ws://proxy:3800/tunnel/connect?token=$VISIONA_PAIRING_TOKEN
[remote-proxy]
PairingStore.Validate(token):
if token == env.VISIONA_PAIRING_TOKEN:
return {UserID: env.VISIONA_STATIC_USER_ID, TokenHash: sha256hex(token)}
else:
return ErrInvalidToken
```
**雛形限制**
- 單 token 單 user永不過期無法 revoke
- **不做 Stage 2 升級**pairing 和 session 合併為同一個 token端點收到任何 `vAc_*` 格式就接受)
- 只適合 devPhase 1 切入正式 DB-backed 前,必須先改為 Stage 1 + Stage 2 兩階段(見 §1.2、ADR-003
### 1.2 Phase 1DB-backed + 兩階段 Token
詳見 ADR-003。雛形 → Phase 1 流程定義如下:
#### Stage 1 — Pairing Token短期**15 分鐘 TTL**,一次性)
使用者於 Web 頁面點「Pair New Device」
```
POST /api/pairing/token
Auth: bearer <user JWT>
Response 200:
{
"token": "vAc_a1b2c3d4e5f6...", // 明文只此一次vAc_ + 32 hex共 36 字元)
"expires_at": "2026-04-21T13:15:00Z" // 15 分鐘後過期
}
DB 寫入 pairing_tokens:
token_hash = sha256(plain)
user_id = <from JWT>
kind = 'pairing'
expires_at = now + 15min
used_at = NULL
```
使用者把 token 貼到 local agent複製貼上、QR code、CLI flag 皆可)。
#### Stage 2 — Session Token長期**90 天 TTL**,可撤銷)
local agent 首次用 Pairing Token 連 proxy**遠端會換發** Session Token
```
WS /tunnel/connect?token=vAc_a1b2c3d4...
remote-proxy (經 PairingStore.Validate):
info := SELECT * FROM pairing_tokens WHERE token_hash = sha256($1)
AND kind = 'pairing'
AND revoked_at IS NULL
AND expires_at > now()
AND used_at IS NULL
IF info NOT FOUND → reject 401
# 原子升級 (single transaction)
BEGIN;
device_id := create_device_if_needed(info.user_id, agent_serial)
session_plain := "vAs_" + hex(crypto/rand(32)) # 64 hex chars
INSERT INTO pairing_tokens
(token_hash, user_id, device_id, kind, parent_token, expires_at, created_at)
VALUES
(sha256(session_plain), info.user_id, device_id,
'session', info.token_hash, now() + 90 days, now());
UPDATE pairing_tokens
SET used_at = now(), device_id = $device_id
WHERE token_hash = info.token_hash;
COMMIT;
# 在 WS upgrade response header 或首個 yamux control frame 回傳:
# { "session_token": "vAs_..." }
accept tunnel
```
local agent 收到後**持久化 Session Token**config / keychain**丟棄 Pairing Token**(已作廢)。日後重連一律用 Session Token`kind='session'` 的 token 重連只需:
```
info := SELECT * FROM pairing_tokens WHERE token_hash = sha256($1)
AND kind = 'session'
AND revoked_at IS NULL
AND expires_at > now()
IF info NOT FOUND → reject 401使用者需在 Web UI 重 Pair
ELSE → accept tunnel + UPDATE last_seen_at
```
**撤銷**:使用者在 Web UI「Devices → Revoke」→ `UPDATE pairing_tokens SET revoked_at = now() WHERE token_hash = ?`。下次連線驗證即被拒。
### 1.3 Token 格式2026-04-22 M-1 修訂:統一為 hex + 前綴)
```
vAc_[0-9a-f]{32} # Admin-Credentialpairing共 36 字元
vAs_[0-9a-f]{64} # Agent-Sessionsession共 68 字元
```
- 字元集:**小寫 hex + 底線前綴**(不用 base64url避免 URL-safe 混淆 / 大小寫歧義)
- 產生:`crypto/rand.Read``hex.EncodeToString`
- **API 回傳純字串,無空格、無分隔符**(前端 UI 顯示時可每 8 字元插空格提升可讀性;複製/貼上時前端需 `.replace(/\s/g, '')` 正規化)
- 正則驗證:
- Pairing`^vAc_[0-9a-f]{32}$`
- Session`^vAs_[0-9a-f]{64}$`
- 前綴讓 log / debug 一眼看出類型log 永遠只記前 8 字元前綴(例:`vAc_a1b2c3d4...`
### 1.4 Hand-shake 額外欄位Phase 1
WS upgrade 前local agent 可帶附加 header 宣告自己:
```
GET /tunnel/connect?token=vAc_a1b2c3d4... HTTP/1.1
X-Agent-Version: local-tool 1.2.3
X-Agent-OS: darwin/arm64
X-Agent-Serial: KL520-1234-ABCD
```
proxy 驗證 + 記錄到 `pairing_tokens.device_id` 綁定。
---
## 2. Auth使用者登入
### 2.0 介面雙層2026-04-22 新增 M-3
Auth 切分為兩層 interface對應不同生命週期 / 呼叫場景:
| 介面 | 層級 | 呼叫時機 | 方法 |
|------|------|---------|------|
| `AuthService` | **Middleware 層** | 每個 HTTP request 進來時middleware| `Authenticate(req) → UserContext``Authorize(ctx, resource, action)` |
| `AuthProvider` | **Handler 層** | 使用者登入 / 註冊 / 登出等明確動作 | `Register``Login``Logout``ValidateToken``GetUser` |
雙層並存原因:
- `AuthService` 著重「這個 request 代表哪位使用者」(狀態萃取),每個 request 都要跑
- `AuthProvider` 著重「使用者的 Auth 生命週期管理」(帳號 CRUD + token 簽發),只在登入登出等動作發生
實作時通常一個底層 providerClerk / OIDC client會同時被 `AuthProvider``AuthService` 包裝;但 interface 分開讓 handler 不需要知道 middleware 的細節。
```go
// internal/auth/service.gomiddleware 層)
type AuthService interface {
Authenticate(ctx context.Context, req *http.Request) (*UserContext, error)
Authorize(ctx context.Context, userCtx *UserContext, resource, action string) error
}
// internal/auth/provider.gohandler 層2026-04-22 新增)
type AuthProvider interface {
Register(ctx context.Context, req *RegisterRequest) (*User, error)
Login(ctx context.Context, req *LoginRequest) (*LoginResult, error)
Logout(ctx context.Context, token string) error
ValidateToken(ctx context.Context, token string) (*UserContext, error)
GetUser(ctx context.Context, userID string) (*User, error)
}
type LoginRequest struct { Email, Password string }
type LoginResult struct { User *User; AccessToken, RefreshToken string; ExpiresAt time.Time }
type RegisterRequest struct { Email, Password, Name string }
```
### 2.1 雛形
> **⚠️ 已被 OIDC 取代2026-04-26 / OB5**
>
> 本節描述的 `StaticAuthProvider` / `StaticAuthService` 已從 codebase 移除。
> Phase 0.6 起 visionA-backend 的唯一認證路徑是 OIDC接 Innovedus Member Center
> 詳見:
> - [adr-010-oidc-bff.md](./adr/adr-010-oidc-bff.md)(接入策略)
> - [adr-011-supersede-adr-005.md](./adr/adr-011-supersede-adr-005.md)(推翻決策)
> - [oidc-tdd.md](./oidc-tdd.md)(實作細節)
>
> demo-user 仍可用 — Member Center 端 seed `demo@visionA.local / demo123` 帳號。
**歷史紀錄(已過時)**
- `/api/auth/login``/api/auth/register` 曾由 `StaticAuthProvider` 實作:
- `Login(email, password)`**任何帳密都通過**,回 `demo-user` + 假的 access token`demo-access-token`
- `Register(...)` — stub`ErrNotImplemented`(前端顯示「即將推出」)
- `Logout(...)` — stub清前端 store 即可)
- `ValidateToken("demo-access-token")` → demo-user其他 token → `ErrInvalidToken`
- `GetUser("demo-user")` → demo-user struct其他 → `ErrNotFound`
- `StaticAuthService.Authenticate` 永遠回 `demo-user`middleware 層;與 provider 結果一致)
- 前端 login 頁面可填任意帳密、點下去直接進入應用(體驗接近真實但零 DB
### 2.2 Phase 1 選項
| 方案 | 優 | 劣 |
|------|----|----|
| 自建 email + password + JWT | 完全掌控 | 要自己做 email 驗證、密碼重設、暴力破解防禦 |
| Clerk / Auth0 / Supabase Auth | 現成、支援社交登入 | 綁定 vendor成本隨用戶增加 |
| OIDC 自建Keycloak / Ory Kratos| 開源、標準 | 維運成本 |
**建議**Phase 1 初期用 Clerk免費額度充足支援 Google/GitHub SSO、Magic Link、MFA成長後視成本再評估遷移。
### 2.3 Session 機制
- JWT`sub=user_id`, `exp`, `roles`)放 `Authorization: Bearer ...` header
- Refresh token 存 HttpOnly cookie避免 XSS 拿到)
- Logout清 cookie + JWT 黑名單Redis
### 2.4 Service-to-Service TokenPhase 0.8 啟用)
> 對齊 [`adr/adr-014-conversion-integration.md`](./adr/adr-014-conversion-integration.md) 與 [`conversion.md`](./conversion.md) §5。
visionA-backend 從 Phase 0.8 起除了「user 端的 OIDC BFF flow」§2.0),還會以**服務身份**呼叫 Innovedus Member Center / FAA / converter — 用 OAuth 2.0 `client_credentials` grant。
#### 2.4.1 流程
```
visionA-backend ──POST {issuer}/oauth/token──► Member Center
grant_type=client_credentials
client_id=<ServiceClientID> ← 預埋於 OIDCConfig.ServiceClientID
client_secret=<ServiceClientSecret> ← 預埋於 OIDCConfig.ServiceClientSecret
scope=converter:job.write converter:job.read files:download.read files:download.delegate
◄─────── { access_token, expires_in, scope }
cacheexp - 15s 內可重用)
```
#### 2.4.2 Cache 策略
- 單一 token cache 涵蓋全部 4 個 scopeMC 端發單一 token 含全部)
- cache 在 visionA-backend 進程記憶體(`sync.RWMutex` 保護)
- `exp - 15s` 提前重取,避免下游使用時剛好過期
- 併發保護double-checked locking第一次 cache miss 時其他 goroutine 等待)
- 重啟即清空in-memory無持久化下次需要時重新取
#### 2.4.3 失敗處理
| 場景 | 處理 |
|------|------|
| 4xxclient_id / scope 設定錯)| fatallog + 5xx response 給上游 caller**不重試** |
| 5xx / network | 指數退避 max 2 次1s, 2s|
| Token cache 命中 → 但 MC 已 invalidaterare | 下次 401 時 force refresh + 重打 |
#### 2.4.4 Secret 管理
- `VISIONA_OIDC_SERVICE_CLIENT_SECRET` **絕不可** commit 進 git`.gitignore``.env`
- prod 用 AWS Secrets Manager / k8s Secret 注入
- log 永遠不印完整 token只印前 8 字元前綴(`Bearer ey1234...`
- 若 secret 洩漏MC 端 rotate → 重新部署 visionA-backendin-memory cache 自然失效
#### 2.4.5 Delegated Download Token
不同於上面的 service tokenFAA delegated download token 是「visionA backend 用自己的 service token 跟 MC 換來、再轉發給 browser 直連 FAA 用的短期 opaque token」
```
visionA backend ──POST /file-access/download-tokens (Bearer service-token, scope=files:download.delegate)──► MC
body: { tenant_id, user_id, object_key, method:"GET", expires_in_seconds:300 }
◄─────── { token, url, expires_at }
visionA backend ──回 frontend──► { download_url, expires_at }
Browser ──GET /files/{key}?access_token=<delegated>──► FAA
└──► MC validate token
```
安全特性:
- TTL 短(預設 5 分鐘 / 範圍 60-900s`VISIONA_FAA_DELEGATED_TTL_SECONDS` 控制)
- 綁定 method=GET + 特定 object_keyFAA 拒絕其他 method 或 key
- visionA-backend 必須先做 ownership 檢查job_id ↔ user_id mapping才換 token
- frontend **不可快取** download_url每次「下載」按鈕重打 backend 換新 token
#### 2.4.6 Trust Boundary
| 邊界 | 信任方向 | 守護機制 |
|------|---------|---------|
| Browser → visionA-backend | visionA-backend **不信任** browser 帶的 user_id / object_key | 一律從 OIDC cookie session 取 user_idobject_key 從內部 mapping 反查 |
| visionA-backend → converter | converter **完全信任** visionA-backend 帶的 user_id | visionA-backend 是唯一灌 user_id 的點multipart streaming 重組時黑名單 client 帶的 user_id|
| visionA-backend → FAA | FAA 不認 visionA 而是線上跟 MC validate token | service token (s2s pull) + delegated token (browser direct) 都走 MC |
| visionA-backend ↔ MC | MC 用 client_secret 認 visionA 服務身份 | 標準 OAuth 2.0 client_credentials |
---
## 3. 傳輸加密
| 連線 | 加密 | 備註 |
|------|------|------|
| Browser → api-server | HTTPS | Phase 1雛形 HTTP |
| Browser → api-server WS | WSS | 同上 |
| api-server ↔ remote-proxy內部 HTTP| HTTPVPC-only | 如 public 需 mTLS |
| Local agent → remote-proxy | WSS | Phase 1雛形 WS |
| Local agent → Kneron USB | 無(硬體直連)| — |
---
## 4. Rate LimitingPhase 1
| 端點 | 限制 |
|------|------|
| `POST /api/auth/login` | 5 / min / IP |
| `POST /api/pairing/token` | 5 / min / user |
| `WS /tunnel/connect` | 10 / min / IPtoken 級限制另計 |
| 一般 `/api/*` | 100 / min / user |
**雛形不做**。記錄為 TODO。
---
## 5. CORS
```go
// internal/api/middleware/cors.go
func CORSMiddleware(allowedOrigins []string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if allowed(origin, allowedOrigins) {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
}
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Max-Age", "86400")
if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204); return }
c.Next()
}
}
```
**雛形**`allowedOrigins = ["*"]`(全放)
**Phase 1**:白名單 `["https://app.visiona.cloud"]`
---
## 6. WebSocket Origin Check
Browser 連 `/ws/*` 時,遵循 CORS origin checklocal-tool 的 `origin.go` 已成熟;搬過來)。
`/tunnel/connect` 是 local agent 連進來(非瀏覽器),**Origin check 跳過**(該 endpoint 僅 token 驗證)。
---
## 7. CSRF
- JWT-in-header非 cookie→ 天然免疫 CSRF
- 若 Phase 1 用 session cookie
- `SameSite=Lax` cookie
- `CSRF-Token` headerdouble-submit pattern
---
## 7.1 Cookie Session 設計取捨Phase 0.6 OIDC BFF
Phase 0.6 OIDC BFF 落地後OB1-OB6Cookie session 設計做出以下取捨:
### Pending 與 Logged-in Session 共用同一個 CookieADR-012
`oidc-tdd.md §4.5` 原設計示意了兩個 cookie`visiona_pending_sid` 短 TTL + `visiona_session` 長 TTL。實作時為了減少 cookie 數量與 handler 邏輯,改採**合一**:兩種狀態共用同一個 `visiona_session` cookie + 同一個 `usersession.Store` record`Session.UserID` 是否為空區分階段。
**安全防護三道線**
| # | 防線 | 位置 | 說明 |
|---|------|------|------|
| 1 | Middleware 強制檢查 UserID 空 → 401 | `internal/api/middleware.go` AuthMiddleware | Pending session 訪 protected endpoint 一律拒絕;該檔 169 行附「安全臨界檢查」註解明確警告不可拿掉 |
| 2 | Login 完成後 rotate session ID | `internal/usersession/manager.go` RotateSessionID + `internal/api/oidc_auth.go` callback | OWASP ASVS V3.2.1 — 防止攻擊者預先誘騙受害者使用攻擊者 cookie 走完 OIDC flow |
| 3 | Pending state 在 callback 同一次 UpdateSession 清空 | `internal/api/oidc_auth.go` callback | OIDCState / OIDCNonce / OIDCCodeVerifier / Extra["return_to"] 在寫 user info 同一次 commit 中清掉 |
詳見 `adr-012-pending-session-shared-cookie.md`
### Session ID Rotation 觸發點
| 事件 | 是否 rotate | 理由 |
|------|------------|------|
| 登入完成OIDC callback | ✅ rotate | OWASP ASVS V3.2.1,防 session fixation |
| 提權(雛形無此情境) | — | Phase 1 接 RBAC 後可能需要 |
| Logout | ❌(直接刪除) | 不需要 rotate整個 session record 從 store 消除 |
| 一般 API 請求 | ❌ | 維持 session ID 穩定,只刷 LastSeenAt |
### Cookie 屬性最小集(雛形 + Phase 1 共通)
| 屬性 | 值 | 理由 |
|------|----|----|
| `HttpOnly` | true | 阻止 JS 讀取XSS 防護) |
| `Secure` | trueprod/ falsedev HTTP | 只在 HTTPS 傳送 |
| `SameSite` | Lax | CSRF 基線防護Strict 會擋掉 OIDC callback redirect |
| `Path` | `/` | 全站可用 |
| `MaxAge` | 86400雛形 24h | Phase 1 將擴至 7d 並接 refresh token |
| HMAC-SHA256 簽章 | SigningKey ≥ 32 bytes | 防 cookie 偽造NewManager 啟動時強制檢查 |
---
## 8. 輸入驗證
- API body 用 `binding:"required"` + `validator.v10`
- 檔案上傳:
- Size 限制(雛形:模型 500MB 上限)
- Content-Type 白名單(`application/octet-stream`, `application/x-nef` 等)
- Magic bytes 檢查Phase 1
- Pairing token`^vAc_[0-9a-f]{32}$`
- Session token`^vAs_[0-9a-f]{64}$`
- 傳入前先 `.replace(/\s/g, '')` 正規化(允許使用者貼上含空格的顯示格式)
- SQL一律用參數化 query`$1` / prepared statement
---
## 9. Secret 管理
| Secret | 雛形 | Phase 1 |
|--------|------|--------|
| Pairing tokenstatic| env var | DBhash|
| Storage signing key | env var | env var + 定期 rotation |
| DB password | — | AWS Secrets Manager / Vault |
| JWT signing key | — | 同上 |
| S3 credentials | env vardev| IAM roleEC2 / K8s SA|
**禁止**:任何 secret 寫死進程式碼 / commit 到 Git。`.gitignore` 必須含 `.env``data/``*.pem`
---
## 10. 審計 LogPhase 1
關鍵事件記入 `audit_logs` 表:
- 使用者登入 / 登出 / 密碼變更
- Pairing token 建立 / 撤銷 / 使用
- Device 建立 / 刪除
- Model 上傳 / 刪除
- Cluster 建立 / 推論
欄位:`id, user_id, action, resource, details_json, ip, user_agent, created_at`
---
## 11. 第三方依賴安全
- `go mod audit`Go 1.26+)或 `govulncheck` 定期掃
- npm: `npm audit` / Dependabot
- Renovate / Dependabot 自動 PR
- CI 時 fail on known CVE
**雛形不強制**,但建議 CI 設定。
---
## 12. 威脅模型STRIDE 摘要)
| 威脅 | 雛形影響 | Phase 1 防護 |
|------|---------|------------|
| **Spoofing** — 冒用 pairing token | Dev 環境可接受 | DB-backed + 兩階段 token + rate limit |
| **Tampering** — 中間人改 payload | 無 TLS局網 OK公網不行 | 全程 TLS |
| **Repudiation** — 否認操作 | 無 audit log | audit_logs 表 |
| **Info Disclosure** — token / model 洩漏 | env 獨立隔離 | TLS + DB 加密 + presigned URL 有限 TTL |
| **DoS** — 灌爆 tunnel | 無防護 | rate limit + connection pool |
| **Elevation of Privilege** — 讀他人 device | OIDC 已實裝OB5 後 user_id = OIDC sub | RBAC + repository 查詢一律帶 `WHERE owner_user_id = ?` |
---
## 14. 前端雛形安全債Phase 1 必還)
F5F6 實作留下的**已知安全債**。雛形階段可接受開發者自用、local 後端),但 Phase 1 對外開放前必須逐項處理。
| # | 項目 | 現況(雛形) | Phase 1 處理 | 對應程式碼 |
|---|------|-------------|-------------|-----------|
| 14.1 | ~~Access token 存 localStorage~~ | ~~`visionA.auth.token` key 寫入 localStorage被同網域任何 XSS 讀走~~ **2026-04-26 已修OF2**Phase 0.6 OIDC BFF 後frontend 不再持有 token改用 `HttpOnly; Secure; SameSite=Lax``visiona_session` cookie + server-side sessionin-memory。auth-store 改為 me-based`GET /api/auth/me`api client 以 `credentials: 'include'` 自動帶 cookie。 | — | `src/stores/auth-store.ts``src/lib/api.ts`OF2 重寫) |
| 14.2 | Refresh token 未接上 | login response 的 `refresh_token` 目前**被丟棄**access token 過期使用者被迫重登 | 接 refresh 流程access token 短 TTL ~15min、refresh 長 TTL走 cookie | `src/stores/auth-store.ts:137-173` |
| 14.3 | WS token 走 querystring | `ws://.../ws/xxx?token=<JWT>` — 明文進 server access logNginx / LB / Cloudflare歷史 log 含 token 字串 | 改 `Sec-WebSocket-Protocol: Bearer,<token>` subprotocol或 HTTP 先換 ~60s ticket 再 `?ticket=...` | `src/hooks/use-websocket.ts:88-99` |
| 14.4 | Device session 識別與 pairing token 型別未分離 | session-store 的 `activeSessionToken` 命名容易混淆 pairing / session token 語意 | 拆為 `activeDeviceSessionId`(不是 token 而是 id明確區分「識別」與「密鑰」 | `src/stores/session-store.ts:47-52` |
| 14.5 | CSP 尚未設定 | Next.js 預設無 CSP header | 加 strict CSP`script-src 'self' 'nonce-xxx'`);禁止 inline eval | `next.config.ts`(尚未存在) |
| 14.6 | CSRF 防護 | 無(因尚未有瀏覽器端 cookie auth | 切到 cookie auth 時補上 double-submit token 或 SameSite=Strict | 全域 middleware |
**追蹤責任**:每一項在 Phase 1 Sprint Planning 前必須有對應 ticket不允許以「雛形 OK」為由直接上線。
---
## 13. 合規
雛形期**不處理 GDPR / CCPA**。Phase 1 上線前必做:
- [ ] Privacy Policy
- [ ] Terms of Service
- [ ] Cookie Policy
- [ ] 資料匯出 / 刪除GDPR right to access / erasure
- [ ] 若有歐洲用戶DPA with processorsAWS / S3 供應商 / Email 供應商)
---
**雛形實作重點**
- `StaticPairingStore` + env token
- ~~`StaticAuthService` + hard-coded demo user~~ → **已替換為 OIDC**Phase 0.6 / OB5見 oidc-tdd.md
- CORS 限定白名單OB5 已調整)
- HTTPdev/ HTTPSprod 必須)
**Phase 1 必做**
- 兩階段 token
- ~~真實 AuthClerk 或 自建)~~ → **已完成Member Center OIDC**
- HTTPS / WSSprod 強制)
- Rate limit
- Audit log
- Refresh token rotationMember Center 暫無 refresh雛形可接受
- Redis / DB-backed session store取代 in-memory