Files
Arcrun/system-dev/docs/3-specs/arcrun/credential-primitives-wasm/credential-store-migration.md
T
uncle6me-web 406f0fedf8 docs(sdd): credential 遷移 SDD 依 T1.5 spike 改寫——Secrets Store 產品換成 Workers per-script Secrets
依據 T1.5 spike 四驗證全過(2026-07-03,證據 Gitea Arcrun#2):
① API 動態加 secret 免重部署即讀 ② env[ref] 動態字串索引可行
③ 14 把未觸上限(D21 單用戶:記錄即可)④ 重部署後 secrets 存活。

- §2.3 全段改寫:寫入走 CF API PUT /workers/scripts/{script}/secrets、
  讀取走 env[ref]、原三條候選路徑待決撤銷(T1 spike 實證 Secrets Store
  binding 靜態 + .get(ref) 參數被忽略 + REST 唯寫,路徑不成立)
- §2.5 改寫:取值=env[secret_ref](零網路呼叫),wasi-shim
  secret_get(ref) host 端實作=env[ref]
- T3 改寫:ensure store + 注入 store_id 整步取消,改為確保 secrets
  寫入 token/account id 就緒+非關鍵 worker 跑一次 acr update 驗存活
- T4 改寫:secret_get(ref) 實作=env[ref](spike ② 已證)
- T1 標完成(負結果)、T1.5 標完成、T2-T9 解凍施工
- 其餘矛盾句對齊用詞(§0/§2.1/§2.2/§2.4/§3/§4/§5/§6 Q-a/Q-d、
  回填端點更名 migrate-to-workers-secrets)
- 雙讀過渡、回填、回滾、治理(D1 只存 metadata+secret_ref)設計不動

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:22:38 +08:00

183 lines
18 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.
# Credential Store 遷移 SDD — KV → D1(目錄)+ CF Workers per-script Secrets(密文)
> 建立:2026-06-29 by arcrun CC|對應 issueArcrun#13(優先序 3)|決策:leo 2026-06-29 拍板(D19
> **修訂 2026-07-03T1.5 spike,證據 Arcrun#2**:密文的家由「CF Secrets Store 產品」改為「**CF Workers per-script Secrets**」(`wrangler secret put` / API `PUT /accounts/{id}/workers/scripts/{script}/secrets` 那套)。原因:T1 spike 實證 Secrets Store 的 Worker binding 部署時靜態宣告、`.get(ref)` 參數被忽略,不支援「runtime 依 D1 ref 動態查任意 secret」;T1.5 spike 四驗證全過(API 動態加免重部署/`env[ref]` 動態索引可行/14 把未觸上限/重部署後存活),方向定案。§2.3、§2.5、T3、T4 已依此改寫;**雙讀過渡、回填、回滾、治理(D1 只存 metadata+secret_ref 不存密文)設計不動**。D212026-07-02 leo 拍板):當作 SaaS 不存在,按單用戶自架設計,多租戶段落標「future SaaS 再議」。
> 範圍宣告:本檔是既有 SDD `credential-primitives-wasm/` 的補充設計(rule 02 §4.3 例外:現有 SDD 目錄內新增單檔)。**不施工,先 SDD,總管審對齊後放行。**
> 取代關係:本檔的儲存決策**取代** `credential-store-redesign.md` §4.5 的「維持 KV」推薦——leo 看完該推薦後**拍板走 D1+Secrets Store**D19;密文家其後依 T1.5 spike 修訂為 Workers per-script Secrets,見上方修訂註,D19 原則不變),故 Phase B 翻回「搬」。redesign.md 的 Atelegram 一致性,已做)/ C(友善前門)/ Q1(acr parts)不受影響。
> 詞彙:component=TinyGo WASMrecipe=http_request+固定設定|workflow=多步 YAMLauth-recipe=credential 怎麼注入。
---
## 0. leo 的最終決策(D19,一句話)
**「擁有目錄,不擁有內容物」**arcrun 持有 credential 的**目錄**(看得到存了哪些、最後何時用),但**不持有、也讀不回密文明文**——連 owner 自己的界面都讀不到值。
動機(leo 親述):用戶 API key 外泄被盜刷的經濟損失 + 商譽風險(「就算不是我們過失也會怪在我們身上」)。**我們不持有明文、不持有金鑰 = 不背這個鍋。**
```
密文值(token / SA JSON / private key → CF Workers per-script SecretsCF 託管,API 唯寫,arcrun 拿不到明文)
credential 目錄(name/service/metadata → 使用者自己的 D1(不含密文,只含指向 Workers Secrets 的 secret_refenv var 名)
舊自管 ENCRYPTION_KEY + 密文存 KV → 廢掉(回填完成後);KV 降為暫存/熱讀快取,非真相源
界面 acr creds list → 讀 D1(顯示清單 + last_used),看得到但讀不回值
```
### n8n credentials UXD19 定案)
- 界面顯示**很多 credentials**(名字 / 服務 / metadata / last_used)。
- **不能 read 既有值、不能 edit 既有值**(連 owner 都不行)。只能:
- **整筆 replace**(重貼新值覆蓋舊的)
- **delete**
- read 不回值「不是限制、是設計」——值在 Workers SecretsAPI 唯寫,T1.5 spike 實證讀不回),arcrun 拿不到。
---
## 1. 現狀(已核實 code2026-06-29
| 項目 | 現狀 | 檔案 |
|---|---|---|
| 密文儲存 | `CREDENTIALS_KV`key=`{api_key}:cred:{name}`,值=`{encrypted, iv}`AES-GCM | `cypher-executor/src/routes/credentials.ts:48` |
| 加密金鑰 | 自管全域 `ENCRYPTION_KEY`client 端加密、WASM `crypto_decrypt` 解密) | rule 01 加解密;`wasi-shim.ts` |
| 寫 | `POST /credentials` { name, encrypted, iv } | `credentials.ts:24-53` |
| 讀+解密 | `auth_static_key` WASM `kv_get``crypto_decrypt` | `registry/components/auth_static_key/main.go:146` |
| 列表 | `GET /credentials``KV.list({prefix})` 列名(不含值) | `credentials.ts:68-78` |
| D1 | 全 repo 無 credential D1D1 只有 KBDB graph | `kbdb/wrangler.toml:10` |
**風險點(leo 要廢的)**:自管單一 `ENCRYPTION_KEY` → 漏一把,全部 credential 裸。
---
## 2. 目標架構(D19
### 2.1 三方職責
| 元件 | 存什麼 | 誰拿得到明文 |
|---|---|---|
| **CF Workers per-script Secrets**(掛在 cypher worker 上的 secrets | 密文值本體(token/SA JSON/private key | **只有 CF runtime 在執行期以 env var 注入時**。管理 API 唯寫讀不回值(T1.5 spike 實證),arcrun TS/WASM/owner 界面**都拿不回** |
| **使用者的 D1**(與 KBDB 共用那顆 `arcrun-kbdb` | credential **目錄**`api_key / name / service / sensitivity / created_at / last_used_at / secret_ref`,**無密文** | 全員可讀目錄(非機密),**無人從這拿到值** |
| **CREDENTIALS_KV** | 過渡期:舊自管密文;遷移後:降為可選熱讀快取或廢用 | (遷移後不再是真相源) |
### 2.2 D1 schema(明確不含密文欄)
```sql
-- migrations/0002_credentials.sqlkbdb 同一顆 D1IF NOT EXISTS 冪等)
CREATE TABLE IF NOT EXISTS credentials (
api_key TEXT NOT NULL, -- 租戶
name TEXT NOT NULL, -- credential 名(= auth-recipe required_secrets[].key,如 telegram_bot_token
service TEXT, -- 對應 servicetelegram / notion …),可空
sensitivity TEXT NOT NULL DEFAULT 'standard', -- 'standard' | 'high'high = SA JSON/private key
secret_ref TEXT NOT NULL, -- 指向 Workers Secrets 的 env var 名(如 CRED_TELEGRAM_BOT_TOKEN;不是密文本體)
created_at INTEGER NOT NULL,
last_used_at INTEGER, -- 執行注入時更新(治理面 last_used 顯示用)
PRIMARY KEY (api_key, name)
);
CREATE INDEX IF NOT EXISTS idx_cred_apikey ON credentials(api_key);
```
> ⚠️ **「name 是名字標籤不是密文」**leo 防誤解):`name`/`secret_ref` 都是字串標籤,**密文值永不進 D1**。
### 2.3 Workers per-script Secrets 整合(密文存 cypher worker 的 secrets、D1 存 ref
> 依據:T1.5 spike 2026-07-03,證據 Arcrun#2curl/wrangler 原始輸出)。本節取代原「CF Secrets Store 產品」設計——T1 spike 實證該產品的 binding 部署時靜態宣告、`.get(ref)` 參數被忽略、REST 唯寫,不支援「runtime 依 D1 ref 動態查任意 secret」,整案曾停等重新設計;總管裁決轉向 Workers per-script SecretsT1.5 四驗證全過後定案。
**已實證的 Workers per-script Secrets 形態(T1.5 spikeArcrun#2):**
- **寫入(動態、免重部署)**:CF API `PUT /accounts/{account_id}/workers/scripts/{script_name}/secrets`body `{"name":"<secret_ref>","text":"<值>","type":"secret_text"}`(等價 CLI`wrangler secret put <NAME>`,不需本地源碼)。**不改 wrangler.toml、不重上傳 code**spike ① 實證寫入後下一次 invocation 立即可讀(連續 5 次呼叫一致,無延遲、無需等 CF roll 版本)。
- **讀取(runtime 動態 by ref**secret 以 env var 形式進 worker`env` 是普通 JS 物件 → **`env[secret_ref]` 用字串動態索引**(spike ② 實證,`?which=SECRET_B` 正確取回對應值)。這正是 Secrets Store binding 做不到的事(T1 spike ④-b`.get(ref)` 參數被忽略)。
- **唯寫**:管理 API 只能 create/update/delete/list 名字,**讀不回值**(與 Secrets Store 同性質)→ D19「不擁有內容物」保持。
- **`secret_ref` 命名規則照舊**`secret_ref`env var 名(如 `CRED_TELEGRAM_BOT_TOKEN`),D1 目錄存 ref 不存值(§2.2)。命名需避開 cypher worker 既有 vars/bindings 名(施工時以 `CRED_` 前綴隔離命名空間)。
- **數量上限(D21 降級:記錄即可,不做阻擋設計)**:spike ③ 實測 14 把全部 HTTP 201、讀取正常,未觸上限;官方書面上限數字未取得(文件頁被 proxy 擋)。self-hosted 單用戶場景不視為阻擋項;多租戶天花板=future SaaS 再議,不為它增加複雜度。
- **重部署存活**:spike ④ 實證 `wrangler deploy` 重上傳 code 後 14 把 secret 全數存活、值不變——secrets 是掛在 script 外部的獨立資源,不走 wrangler.toml,理論上不受 `stripOfficialOnlyBindings` 注入邏輯影響(不重演 `[ai]` binding 被清的坑)。⚠️ 誠實註記:spike 測的是 `wrangler deploy` 非實際 `acr update` CLI(底層同一支 `PUT /workers/scripts/{name}` API,推論成立但非直接實測)——正式導入時於非關鍵 worker 跑一次 `acr update` 全流程驗證(併入 T3)。
**寫入路徑的 token 需求**cypher worker 需持有一把「能打 Workers Scripts secrets API 的 CF API token」+ account id(寫入/刪除用;本身高敏感,走 `wrangler secret put` 注入 cypher worker,非進 git)。這仍比「自管全域 ENCRYPTION_KEY」好——CF 託管值、token 可輪替、且該 token 讀不回任何 secret 值(API 唯寫)。
**self-hostedleo21c**per-script secrets 是 Workers 原生能力(就是現行 `ENCRYPTION_KEY` 住的機制),任何 CF 帳號皆支援、**無 store 資源需 ensure**(對比原 Secrets Store 設計省去「acr init/update ensure store + 注入 store_id」整步)。`acr init/update` 只需確保上述寫入 token/account id 設定就緒(見 T3)。
> 註:這與「Anthropic Routine env-var 型 self-hosted 不支援」是**不同層**(那是 Anthropic 雲端;這是 CF 帳號能力)——別混(leo 提醒)。
### 2.4 寫入流程(client 加密 → Workers Secrets
> ⚠️ 鐵律對齊:現行是 **client 端加密 + 自管 ENCRYPTION_KEY**。走 Workers Secrets 後,**密文交給 CF 託管**arcrun 不再自管金鑰。client 端是否仍加密一層 = 待決(見 §6 開放問題 Q-b):
> - 選項甲:client 不再加密,明文值經 TLS 送到 cypher → cypher 用「能打 secrets API 的 token」`PUT` 進 Workers SecretsCF 端託管)。**arcrun TS 短暫經手明文**(記憶體中,不落地)。
> - 選項乙:保留 client 加密當「傳輸期保護」,但這需 arcrun 持有解密金鑰才能寫進 secrets 明文槽 → 又回到自管金鑰,違 D19。
> → 傾向**選項甲**(符合「不持有金鑰」),但「TS 短暫經手明文」需 leo 接受(mindset §7 誠實標:不是零接觸,是不落地、不持久、不持金鑰)。
### 2.5 讀取/注入流程(執行期 by ref)
> 依據:T1.5 spike 2026-07-03Arcrun#2)——`env[ref]` 動態字串索引實證可行(spike ②),取值不再需要任何對外 REST 呼叫(值已由 CF runtime 以 env var 注入 worker)。
1. cypher 執行 workflow,遇 recipe `auth_service='telegram'`
2. 查 D1 `credentials` where api_key+name=`telegram_bot_token` → 取 `secret_ref` + `sensitivity`
3.`secret_ref` 在執行期動態取值:**`env[secret_ref]`**Workers Secrets 以 env var 注入,字串索引原生可行;零網路呼叫、零延遲)。
4.`auth_recipe:telegram``inject.path/header` 注入(與現行 auth-dispatcher 同型,只是值來源從「KV 密文+WASM 解密」換成「`env[ref]` 取值」)。
5. 更新 D1 `last_used_at`
> ⚠️ rule 02 §2.2/§2.3 對齊:解密/注入仍不在 cypher TS 實作業務邏輯。`env[ref]` 取值是「host 能力」(類比 `kv_get`/`crypto_decrypt`),落在 wasi-shim host function(新增 `secret_get(ref)`host 端實作=`env[ref]`spike ② 已證此場景成立),**WASM 零件仍是注入邏輯的所在**。施工時須守此界線(見 §5)。
---
## 3. 治理 / UX 端點(n8n 模式)
| 端點 | 行為 | 對齊 D19 |
|---|---|---|
| `GET /credentials` | 讀 **D1**,回 `[{name, service, sensitivity, created_at, last_used_at}]`**不含 secret_ref 對外、不含值** | 顯示清單 + last_used |
| `PUT /credentials/:name`(replace | 整筆覆寫:寫新值進 Workers SecretsAPI `PUT` 同 ref 覆蓋或新 ref+ 更新 D1 metadata | 只能 replace |
| `DELETE /credentials/:name` | 刪 D1 row + 刪 Workers Secrets secretAPI `DELETE .../secrets/{ref}` | 可 delete |
| ~~`GET /credentials/:name/value`~~ | **不存在 / 移除任何讀回值的路徑** | 讀不回值 = 設計 |
CLI 薄殼(rule 07):`acr creds list`(讀 D1 顯示)、`acr creds replace <name>`(覆寫)、`acr creds delete <name>`。**移除任何「印出 credential 值」的路徑**。
---
## 4. 遷移:雙讀過渡 + 回填 + 回滾
### 4.1 雙讀過渡(不停機)
執行期取 credential 值的順序:
1. 先查 D1 有無 `secret_ref` → 有 → 走 Workers Secrets 取值(`env[ref]`,新家)。
2. 找不到 → fallback 舊路徑:`CREDENTIALS_KV {api_key}:cred:{name}` + WASM `crypto_decrypt`(舊自管)。
→ 新寫入一律走新家;舊資料未回填前仍可讀。
### 4.2 回填(一次性、冪等、可審)
`POST /credentials/migrate-to-workers-secrets`(類比現有 `migrate-cron-index` 冪等端點;端點名隨 2026-07-03 修訂由 `migrate-to-secrets-store` 更名,設計不變):
- 對每個 `{api_key}:cred:{name}` KV rowWASM 解密取明文 → `PUT` 進 Workers Secrets(領 `secret_ref`env var 名)→ 在 D1 建 row(含 ref + metadata)。
- 冪等:D1 已有該 (api_key,name) row 且 ref 可解析 → 跳過。
- **誠實回報**逐筆 ok/fail(不假綠,mindset §7)。
- **回填完成且驗證通過後**,才執行 §4.4 廢 ENCRYPTION_KEY。
### 4.3 回滾錨點
- 回填**不刪 KV 舊密文**(保留為回滾錨點),只新增 D1+Workers Secrets。
- 出問題 → 雙讀 fallback 自動回到 KV 路徑;D1 row 可刪、Workers Secrets secret 可刪 → 回到純 KV 狀態。
- 只有在「全租戶回填驗證綠 + 觀察期無 fallback 命中」後,才進 §4.4。
### 4.4 廢自管 ENCRYPTION_KEY(最後一步)
- 移除 `crypto_decrypt` 對 credential 的依賴路徑(KV 密文路徑停用)。
- `wrangler secret delete ENCRYPTION_KEY`cypher / auth_static_key / auth_service_account)。
- 清理 KV 舊密文(確認 Workers Secrets 是唯一真相源後)。
- ⚠️ **此步不可逆**,須 leo 明示放行 + 回填觀察期通過。
---
## 5. 與鐵律 / 既有架構對齊(施工硬約束)
- **rule 02 §2.2/§2.3**cypher TS 不實作 credential 解密/注入業務邏輯。Workers Secrets 取值走 host function 邊界(新增 `secret_get(ref)` 於 wasi-shimhost 端實作=`env[ref]`),注入仍在 WASM auth primitive。
- **rule 01 加解密**:自管 AES-GCM 路徑在回填後廢除;新家由 CF Workers Secrets 託管(arcrun 不持金鑰,管理 API 唯寫讀不回值)。
- **不新增 component**mindset §1);不新增 service bindingrule 03 §3.1)。
- **薄殼(rule 07**CLI/MCP 只做「list 顯示 / replace 覆寫 / delete」介面轉換,secrets 能力在 API。
- **部署繞開 GitHub Actions**D1 migration + secrets 寫入設定走 `acr init/update`wrangler 直推),不掛 Actionssecrets 本體是掛在 script 外部的獨立資源,重部署不受影響(T1.5 spike ④)。
- **D1 同源**:用使用者既有 `arcrun-kbdb` D1(不新建第二顆),migration `0002_credentials.sql` 走現有 `deploy.ts` migration 注入路徑(與 `0001_base.sql` 同模式)。
---
## 6. 開放問題(施工前須 leo / 總管拍板)
- **Q-a(核心)**:✅ **已解(T1 + T1.5 spike,證據 Arcrun#2**。原問「Secrets Store 動態 by-ref 取值路徑是否可行」→ T1 spike 負結果(binding 靜態、`.get(ref)` 參數被忽略、REST 唯寫),該產品路徑不成立;總管裁決轉向 **Workers per-script Secrets**T1.5 spike 四驗證全過(§2.3),硬前置解除。
- **Q-b**:寫入時 client 是否仍加密一層(§2.4 甲/乙)。傾向甲(不持金鑰,TS 短暫經手明文不落地)——需 leo 接受此誠實 trade-off。
- **Q-c**`sensitivity` 分級的判定(誰標 high/standard)——auth-recipe 可加 `sensitivity` 欄宣告該 service 的 credential 等級,或一律 high。
- **Q-d**:與 redesign.md C(友善前門:.env → 一次填)整合——C 的「值來源 .env」在新架構下是「.env 明文 → cypher `PUT` 進 Workers Secrets」,與 §2.4 甲一致。確認 C 依附本 SDD(值的家是 Workers Secrets 不是 D1)。
---
## 7. 任務分解(T1.5 全過,T2-T9 解凍施工——2026-07-03,依總管 Arcrun#2 裁決)
- [x] T1 spikeleo21c CF 帳號 Secrets Store create store + worker by-ref 取值驗證(解 Q-a)。**結果:負結果,路徑不成立**(binding 靜態宣告、`.get(ref)` 參數被忽略、REST 唯寫;2026-07-02,證據 Arcrun#2)→ 轉向 T1.5。
- [x] T1.5 spike**Workers per-script Secrets 四驗證全過**(① API 動態加免重部署即讀 ② `env[ref]` 動態索引可行 ③ 14 把未觸上限(D21:記錄即可)④ 重部署後存活;2026-07-03,證據 Arcrun#2)。**方向定案,硬前置解除。**
- [ ] T2 D1 migration `0002_credentials.sql`(§2.2+ deploy.ts 注入。
- [ ] T3 `acr init/update` 確保 secrets 寫入設定就緒:cypher worker 持有「能打 Workers Scripts secrets API 的 CF API token」+ account id(§2.3 寫入路徑;per-script secrets **無 store 資源需 ensure**,原「ensure store + 注入 store_id」整步取消)+ 於非關鍵 worker 跑一次 `acr update` 全流程驗證 secrets 存活(補 spike ④ 的誠實缺口)。〔依 T1.5 spike 2026-07-03 改寫,證據 Arcrun#2
- [ ] T4 wasi-shim 新增 `secret_get(ref)` host function(守 rule 02 邊界;host 端實作=`env[ref]` 動態索引,spike ② 已證可行,零網路呼叫)。〔依 T1.5 spike 2026-07-03 改寫,證據 Arcrun#2
- [ ] T5 寫入路徑:`POST/PUT /credentials` 改寫 Workers SecretsAPI `PUT`+ D1 ref(§2.4)。
- [ ] T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → `env[ref]` 取值(§2.5+ 更新 last_used。
- [ ] T7 雙讀 fallback(§4.1)。
- [ ] T8 回填端點 `POST /credentials/migrate-to-workers-secrets`(§4.2,冪等可審)。
- [ ] T9 治理端點 list/replace/delete + 移除任何 read-value 路徑(§3+ CLI 薄殼。
- [ ] T10 回填驗證 + 觀察期 → 廢 ENCRYPTION_KEY(§4.4leo 明示放行)。
> **每個 cred 操作跨 TS / WASM / host-function / 兩個 store,必端到端實測**(防再假綠,mindset §7)。原「T1 不通則整案停」已兌現一輪:T1 負結果 → 整案停 → 總管裁決轉向 → T1.5 全過 → **T2-T9 解凍**。備援(若施工再撞死路):codegen binding + 自動重部署(Arcrun#2 總管裁決的方向 1)。