Files
Arcrun/system-dev/docs/3-specs/archive/credential-primitives-wasm/credential-store-redesign.md
T
Leo 8d8b01d240 docs(sdd): credential-primitives-wasm 封存進 archive(T10 完成,卷已結案)
leo 2026-07-21 明令封存:卷已完成,留主目錄會讓未來 session 誤以為進行中。
最後一項 T10(廢除自管加密金鑰)已於 20c7610 完成(移除約 2500 行)。

- git mv 整卷 → system-dev/docs/3-specs/archive/credential-primitives-wasm/
  design.md status: closed;superseded_by 留空(據實:非被另一卷取代,是機制整個換掉)
- 卷首補「封存時仍未完成的項目」——逐條查 code 核實,不當作完成:
  真缺口=auth_mtls 從未實作、7.6 self-hosted auth 鏈端到端從未驗;
  另有勾選過期(auth_oauth2 其實已完成)與驗收條件已作廢(與現行 rule 07 牴觸)者
- 修好 14 處引用(原盤點 10 處,實際更多):session-start-load-sdd.sh 內容嚴重過期
  (把已完成 Phase 寫成進行中)→ 改為以 frontmatter 為判準;四份 rules、CLAUDE.md、
  BACKLOG、3-specs/README、deploy.ts、credentials.ts、wrangler.toml 逐一按性質處理
- 額外:system-dev/docs/2-architecture/ 有四個規則檔重複副本(06-08 遷移遺留)
  → 同步修好,否則留一份過期真相(今日第三次撞到「同一資訊兩份副本」的債)
- 02-forbidden.md §2.1 列的三個「待刪違規 TS」其實早已不存在
  → 改標刪除線+禁止重新引入(過期文件同時製造假待辦與假進行中)

驗證:sdd-active-check exit 0(active 仍恰一份=portal-auth);
session-start-load-sdd.sh exit 0;cypher-executor 與 cli typecheck 全綠;
測試 187/188(唯一 fail 為 pre-existing,stash 覆驗相同)。

未動:pre-write-guard.sh 的 KNOWN_SDDS 白名單不含 archive 路徑
(放寬 guardrail 需明確授權,留待決定)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:57:34 +08:00

215 lines
20 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 重設計提案(A telegram 一致性 + B KV→D1 + C 友善前門)
> ⚠️ **歷史記錄**:本檔的儲存決策已被 `credential-store-migration.md` 取代並執行完畢
> (2026-07-20)。文中提及的舊自管金鑰機制**已不存在**,僅供考古,勿依此操作。
> 建立:2026-06-29 by arcrun CC|更新:2026-06-29(依 leo 最終精確 spec 改寫)|對應 issueArcrun#13
> 範圍宣告:本檔是既有 SDD `credential-primitives-wasm/` 的補充設計筆記(不是新 SDD 子系統,rule 02 §4.3 例外)。
> 詞彙(leo 堅持精確):**零件/component=TinyGo WASM**、**recipe=http_request+固定設定(打最終 API**、
> **workflow=多步 YAML(在這層「按名指定要哪個 credential」)**、**auth-recipe=credential 怎麼注入**。
---
## 0. leo 的最終 spec(一句話):**不是拆除,是把前門做友善 + 把 D1 收尾**
**保留全部既有零件,只改「credential 怎麼餵進去」,不改 recipe 本身。**
```
人填 .env / 環境變數 ← 友善前門,任何人都會(本機=.env;雲端=code-on-web 的 Environment 設定,對稱)
↓ acr creds push 保留,但「值的來源」= .env/env-var(不再強迫手寫 credentials.yaml
長久存「我的 D1」 ← 存一次,存進使用者自己的 D1;之後自動,永不重推
↓ workflow YAML「按名」指定要哪個 credential"我要 google 那把")→ 自動從 D1 取
recipe 打最終 API ← telegram_send / notion / gsheets,保留,不刪
```
**對稱性**:本機讀 `.env`;雲端讀 code-on-web 的 Environment 設定。**同機制,只是來源位置不同。**
### 明確「保留、不可刪」
1. **`acr creds push` 保留** —— 手動推單一 credential 仍合法(只是不再是唯一/被迫的路徑)。
2. **最終 API recipe 保留** —— telegram_send / notion / gsheetshttp_request + 固定設定)是打 API 的端點,**不刪**。改的是「credential 怎麼餵」,不是 recipe。
---
## 0.5 Q1 — list 是動態讀 store 還是 hardcoded?(已核實 code2026-06-29
**結論:recipe/auth-recipe 的「正規清單」是動態的;但 `acr parts` 是 hardcoded 且把 5 個 recipe 混進去 → 那 5 個之外的 pushed recipe 在 `acr parts` 看不到。**
| 指令 | 讀哪裡 | 動態? | pushed recipe 會出現? |
|---|---|---|---|
| `acr recipe push` | `POST /recipes` → KV storerecipe.ts:87 | — | 寫入 store |
| `acr recipe list` | `GET /recipes``RECIPES.list({prefix:'recipe:'})`recipe.ts:182 / recipes.ts:260 | ✅ 動態 | ✅ 會 |
| `acr auth-recipe list` | `GET /auth-recipes``RECIPES.list({prefix:'auth_recipe:'})`auth-recipe.ts:42 / recipes.ts:547 | ✅ 動態 | ✅ 會 |
| **`acr parts`** | **hardcoded `BUILTIN_COMPONENTS` 陣列(parts.ts:28-262),完全不 fetch store** | **❌ 靜態** | **❌ 不會** |
- **component(零件=WASMhardcoded 是對的**:零件只能 PR merge 新增(mindset §4 人類閘門),固定、慢增,靜態清單反映真實。
- **但 `acr parts` 把 5 個 recipe 也 hardcode 進去**parts.ts:197-261gmail_send / google_sheets_append / telegram_send / line_notify_send / notion)——**這是 component 與 recipe 的 conflation**。recipe 是動態投稿的(任何人 `acr recipe push`),卻被釘進 `acr parts` 的靜態清單。
- **後果(leo 的擔憂成立,但範圍精確)**:用戶 `acr recipe push my_recipe` → 進 store → `acr recipe list` / `acr recipe search` **看得到**;但 `acr parts` **永遠看不到**(除非改 source 重發 CLI)。「submitted = invisible」**只發生在 `acr parts` 這個面**,不是整個生態壞掉。
- **與 telegram bug 的關係**telegram bug 是另一條軸(seed source vs live KV 漂移,A 已修),但與此**同病**=「hardcoded 清單 ≠ live store」。`acr parts` 列退役/釘選 recipe#13)也是這病的症狀。
### Q1 修法 scope(建議,未在本批改 code)
1. **`acr parts` 移除 5 個 recipe 的 hardcode**parts.ts:197-261),只留**真零件**logic/data/ai/http_request/cron…=WASMPR-only,靜態正確)。
2. **`acr parts` 末尾改成「指路」**:列完真零件後,動態提示「整合類服務 → `acr auth-recipe list`(即時讀 store);API recipe → `acr recipe list`(即時讀 store)」。讓 AI/人從 `acr parts` 被導到動態清單,不在靜態清單找 recipe。
- 進階(可選):`acr parts` 末段直接 fetch `GET /recipes` + `GET /auth-recipes` 把 store 的 recipe 也列出來(標明「動態,來自 store」),徹底消除「submitted = invisible」。
3. **明確分流原則寫進 parts.ts 註解**:零件=靜態(PR-only);recipe/auth-recipe=動態(從 store 讀)。不再把 recipe 釘進零件清單。
4. 這 scope #13「acr parts 殘留」的根治版(#13 只把 recipe 描述改對,沒解決「recipe 不該 hardcode 在 parts」的結構問題)。**屬薄殼修正(rule 07)=把「列 recipe」這能力從介面靜態資料改成讀 API store,不違禁令。**
---
## 1. 現狀(已核實 code + 實測 live2026-06-29
### 1.1 儲存:**KV,不是 D1**(B 的根因=未完成的遷移,非「設計即如此」)
- credential 只存 `CREDENTIALS_KV`key=`{api_key}:cred:{name}`AES-GCM。
- 寫:`acr creds push``POST /credentials``cypher-executor/src/routes/credentials.ts:48`)。
- 讀+解密:`auth_static_key` WASM `kv_get``registry/components/auth_static_key/main.go:146`)。
- binding`cypher-executor/wrangler.toml:23`;型別 `src/types.ts:31`
- **全 repo 無任何 credential 的 D1**D1 引用全是 KBDB graph)。
- leo 確認:採用 KBDB 後使用者已有自己的 D1,當時就要求 credential 也搬進「我的 D1」長久存 → **此遷移從未完成**
→ BKV→D1)**確定要做**,是 leo 明確要的「長久存我的 D1」層。
### 1.2 .env → credential**值不會自動進**(C 的痛點,但前門不友善 ≠ 拆掉)
- `.env` 目前只供「設定」:`ENCRYPTION_KEY`/`NAMESPACE`/`ARCRUN_*`/`CLOUDFLARE_*``cli/src/lib/config.ts:125`)。
- credential 的**值**只來自手寫 `credentials.yaml``acr creds push``cli/src/commands/creds.ts`)。
- leo 記憶「notion/gsheets 只靠 .env 就通」與 code 不符——實際是早先 push 過、值已在 KV。
- 痛點(北極星「拿 credential ≠ 攻打用戶」):每次要手寫 yaml;不像 n8n Credentials tab 填一次長存。
**C 不是發明「自動偵測魔法」**,而是把 `acr creds push` 的**值來源**從「手寫 yaml」改成「讀 .env/env-var」,存進 D1,之後 workflow 按名取。
### 1.3 telegram 漂移(A)——已實測 smoking gun,已修 source
- recipe `telegram_send``api-recipe-seeds.ts:108``auth_service:'telegram'`endpoint `.../bot{{auth.bot_token}}/sendMessage`
-`auth-recipe-seeds.ts` source 只有 **23** 個 service,缺 `telegram`/`line_notify`/`kbdb`/`google_user`
- 實測:prod = 27(手動 seed 過 telegram);self-host **leo21c = 23,無 telegram** → telegram_send 找不到 auth recipe → `{{auth.bot_token}}` 注入空 → URL 變 `.../bot/sendMessage` → 壞。notion/gsheets 通是因它們在那 23 裡。
- 這是「source vs live 漂移=假綠」。telegram **不是**走 legacy `BUILTIN_CREDENTIALS_MAP`,正確形態一直是 recipe+auth-recipe,只是 auth-recipe 沒進 source 種子。
---
## 2. 目標狀態(target,=leo 的 3 層流,全部保留只連得更友善)
1. **唯一打外部 API 的方式** recipe(保留)。telegram 與 notion/gsheets **同一條**「workflow 按名指定 → 從 D1 取 → auth-recipe 注入 → recipe 打 API」鏈,無例外(A,本次已修 source)。
2. **credential 長久存使用者自己的 D1**(如 n8n Credentials tab):填一次、長存、workflow 按名引用、永不重推(B)。
3. **友善前門**:使用者把 token 填進 `.env`(本機)/ code-on-web Environment(雲端)**一次**`acr creds push`(保留)以此為值來源,client 端加密後存進 D1。不再被迫手寫 `credentials.yaml`C)。
---
## 3. 遷移步驟(A 已做;B/C 是設計變更,誠實標工作量)
### Phase A — telegram/line_notify/kbdb 一致性(本次已做,commit 90777e3,未 push/merge
- [x] `auth-recipe-seeds.ts` 補 telegraminject.path bot_token/ line_notifyheader Bearer/ kbdbheader Bearer),形態取自 prod。
- [x] `recipes.ts` `AuthInjectSpec``path?`WASM/SDD §六早有、source 介面漏 → tsc 擋)。
- [ ] google_useroauth2)暫不回灌:內嵌 client_secret 不可進 git + 介面無 oauth2 欄 → 留 Phase D。
- 部署後 leo21c 跑 `acr update`(/init/seed) → 23→26telegram 即與 notion 同鏈可用。
- **不刪任何 recipe;不碰 `acr creds push`。** 純補種子 + 補型別欄位。
### Phase B —(已翻案,見 §4.5)儲存:**KV 維持,不搬 D1**;高敏感可選補 Secrets Store
> 早先版本寫「KV→D1 遷移」。§4.5 核實史實 + leo 自己 2026-06-07 拍板(credential 留 KV)後**翻案**
> credentialRedis-class(按 key 直接點查、密文不可 SQL 查),搬 D1 是淨負債。**B 改為:**
1. **低敏感 credential 維持 KV**(現狀 CREDENTIALS_KVAES-GCM)。`acr creds list` 已可用(`GET /credentials` KV.list)。
2. **(可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Store**credential_parts.md §8.2KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。
3. **不新增 D1 credentials 表、不新增 `cred_get` host function、不做 KV→D1 migration**——這些原計畫取消(好處 KV 已有)。
4. 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets StoreD1 只放非機密 metadata。
5. 風險:跨 TS Worker / WASM / host function 三層,**必端到端實測**(防再假綠,mindset §7)。**非小工程。**
### Phase C — 友善前門:`acr creds push` 值來源=.env/env-var → D1(小~中工程,依附 B)
> **精確(leo 修正)**:不是「自動 pickup 魔法」,是「`acr creds push` 保留,但值的來源改成 .env/env-var,存進 D1,之後 workflow 按名取」。
1. `acr creds push`(保留)新增「從 .env/env-var 讀值」模式:掃 .env 中「已知 credential 名」
(由 auth-recipe 的 `required_secrets[].key` 反推,例 `notion_token`/`telegram_bot_token`)→ client 端加密 → 存進 D1Phase B 的 store)。
- 仍保留現有「從 credentials.yaml 讀值」模式(向後相容,不刪)。yaml 變成可選輸入源之一,不再是被迫。
2. **對稱**:本機讀 `.env``config.ts loadDotEnvOnce` 已會載入 .env 進 `process.env`);
雲端=code-on-web 的 Environment 設定(同樣落到 env-var)。同一套讀法、不同來源位置。
3. 名稱規約:.env 用大寫(`NOTION_TOKEN`)→ normalize 成 credential 名 `notion_token`(=auth-recipe required_secrets.key)。
4. **薄殼界線**rule 07):這是「擴 `acr creds push` 的輸入源」=介面慣例的輸入解析(§2 允許第 1 項),
**不是**在 CLI 拼裝 API 缺的能力(值來源是 .env/yaml 都只是「轉成 API 期望的加密 payload」)。加密仍 client 端(§2 第 4 項唯一例外)。store 仍是 API(D1)。
5. 結果:填一次 .env → `acr creds push`(或 deploy 末尾自動觸發一次)→ 長存 D1 → workflow 按名引用、零重複推。
### Phase D(後續,非本批)— google_user(oauth2) 回灌 + oauth2 secret 部署期注入
- AuthRecipeDefinition 介面補 oauth2 欄位;client_secret 走 `wrangler secret`/env 注入(不進 git),再回灌 source。
---
## 4. 正解:telegram 的 workflow / recipe yaml(給 mira 抄)
telegram 發訊=**recipe `telegram_send`**(不是 component、不是自建零件);**credential 在 workflow 層按名指定**
```yaml
name: notify_done
flow:
- "notify >> ON_SUCCESS >> end" # 單節點時 flow 可省
config:
notify:
component: telegram_send # ← 內建 recipe canonical_id(不是 component:telegram
chat_id: "123456789"
text: "✅ 任務完成"
# bot_token 不寫這裡。recipe 的 auth_service='telegram' → 按名從 D1 取 telegram_bot_token
# → auth-recipe:telegram 的 inject.path 注入進 URL path。一致鏈,與 notion/gsheets 同型。
```
**credential 解析(target 設計):**
1. 使用者把 `TELEGRAM_BOT_TOKEN=123456:ABC...` 填進 `.env`(本機)/ code-on-web Environment(雲端)**一次**。
2. `acr creds push`(保留;值來源=.env)→ client 加密 → 存進「我的 D1」credential store。**之後永不重推。**
3. 執行時:cypher-executor 見 `telegram_send.auth_service='telegram'``auth_static_key` WASM 用 `cred_get` 從 D1 取
`telegram_bot_token` 密文 → `crypto_decrypt` → 依 `auth_recipe:telegram``inject.path.bot_token` 輸出 `auth_path`
`makeRecipeRunner` 把 endpoint `{{auth.bot_token}}` 換成真 token → POST `https://api.telegram.org/bot<token>/sendMessage`body=`{chat_id,text}`
這條鏈與 notionheader 注入)/gsheetsservice_account runtime token 注入)**完全同型**,只注入位置不同(path vs header)。
> **過渡期(B/C 未完成前,今天就能用的方式)**:在 `credentials.yaml` 放 `telegram_bot_token` + `acr creds push`(值進 KV)。
> 上面 yaml 的 workflow 寫法不變——只是 store 暫時是 KV、值來源暫時是手寫 yaml。B/C 完成後 store→D1、來源→.env**workflow yaml 一字不改**(按名引用穩定)。
---
## 4.5 Q2 — KV vs D1:史實核實 + 三方調和 + CC 推薦(leo 授權我決定)
### 史實(git 核實,2026-06-29):**本 repo 的 credential 從來沒在 D1**
- 最早 MVP commit `2707fca`credential 即「AES-GCM decrypt from **CREDENTIALS_KV**」。
- 更早的 u6u-credentials ancestor worker`credentials/` 已刪):`CREDENTIALS_KV.put('cred:${id}')`——**也是 KV**。
(它的 `CredentialRecord` 已有 name/type/created_at 結構欄位,但以 JSON 存進 KV。)
- 全 history 的 `CREATE TABLE` 只有 KBDBentries/templates/entry_values),**從無 credentials 表**。
-**找不到任何「credential D1→KV 撤退」的 commit**。leo 記憶的「原本 D1 有 credentials 表、後來退 KV」
**在本 repo 史實裡不存在**(可能是 pre-repo 設計或別處,沒落地過 code)。
### 三方說法(全部攤開,none 寫 D1 存 credential
| 來源 | 對 credential 儲存的說法 |
|---|---|
| leo 口頭(現在) | 存 **D1**(「長久存我的 D1」) |
| 現行 code | **KV**CREDENTIALS_KVMVP 至今未變) |
| **kbdb-base SDD 拍板 2026-06-07leo 親簽「Leo:同意」)** | **credential 留 KV**(「session、verdict、credential 留 KV=短期高頻純取用,類比 Redisworkflow/recipe/成功記錄才進 D1=要層級/列舉/排序」)。`kbdb-base/design.md:318``tasks.md:11``tasks.md:83` 三處一致。 |
| credential_parts.md §8.2(長期需求源) | **tenant KV + Cloudflare Secrets Store**(低敏感→KV AES-GCM;高敏感如 service account JSON/private key→Secrets StoreKV 只存 ref)。**不是 D1。** |
**leo 口頭「D1」與他自己 2026-06-07 的書面拍板(credential 留 KV)直接衝突**,且兩份書面需求源都沒說 D1。
SDD 協議(00-sdd-protocol「規範互相矛盾→引原文、不自行猜」)要求攤開矛盾;但 leo 此次**明確授權「你決定並推薦」**,故下方給推薦(grounded in 上述證據),同時把矛盾標清楚讓 leo 有機會推翻。
### 「退 D1→KV 的理由還成立嗎?」(leo 的核心提問)
- 本 repo 沒發生過該退守,故無「當初理由」可考。但 leo 假設的理由(「self-hosted 少依賴、免 D1」)**今天確實已 moot**——
self-hosted 用戶現在本來就有一顆 KBDB D1(`kbdb/wrangler.toml:10` `[[d1_databases]]`deploy.ts 注入用戶自己的 id)。
- **但「能用 D1」不等於「credential 該用 D1」。** 2026-06-07 拍板把 KV 留給 credential 的理由不是「沒有 D1」,
而是**「credential =短期高頻純取用(by api_key+name 直接點查),是 Redis-class 工作負載,不需要 D1 的層級/列舉/排序」**。
這個理由**與有沒有 D1 無關,今天仍成立**。
### ✅ CC 推薦(leo 授權我決定):**credential 主存維持 KV;不為「列表/按名引用」而搬 D1**
理由(誠實 trade-offmindset §7):
1. **守 leo 自己的拍板**2026-06-07「credential 留 KV」是有理由的設計(Redis vs PostgreSQL 分工),非偷懶。推翻它要有新理由,目前沒有。
2. **D1 宣稱的好處在 KV 上已可達或不需要**
-`acr creds list`」KV 已能做(`GET /credentials``KV.list({prefix})` 列名,credentials.ts:68-78 已實作,**今天就有**)。
- 「按名引用(workflow 指定要哪把 cred)」是 **key 設計**`{api_key}:cred:{name}`)就給的,與 KV/D1 無關——telegram_send 的 `auth_service``required_secrets.key` 已是按名取。**A 修好後就具備,不需 D1。**
- 「結構化欄位/sensitivity 分級」credential 是不可查詢的密文 blob(查詢明文=洩漏),D1 的 SQL 查詢優勢對 credential **用不上**;分級需求由 credential_parts.md 的 **Secrets Store**(高敏感)解,不是 D1。
3. **搬 D1 成本高且踩鐵律邊界**:要新 host function `cred_get`WASM 改)、一次性 migration、跨 TS/WASM/host-function 三層 e2e——換來的好處 KV 已有。**淨負債。**
4. **真正缺的不是 D1,是「友善前門 C」**:leo 真痛點是「每 cred 手寫 yaml + 重推」,那是 **C.env 當值來源 + 一次存)** 解的,**與 store 是 KV 還 D1 無關**。
**推薦的目標儲存(取代本檔早先「搬 D1」設計):**
- **低敏感 credential(多數 token/api_key)→ 維持 KV**(現狀,AES-GCM)。
- **高敏感(service account JSON / private key)→ Cloudflare Secrets Store**(依 credential_parts.md §8.2KV 只存 ref)——這是**唯一值得做的儲存升級**,且與 leo 拍板不衝突(拍板說 credential 留 KV,未禁高敏感走 Secrets Store;兩者互補)。
- **C(友善前門)照做**`acr creds push` 值來源=.env/env-var、存一次、workflow 按名取。**store 仍 KV(+Secrets Store)**。
-**B 從「KV→D1 遷移」改為「KV 維持 + 高敏感補 Secrets Store(可選、後續)」**。telegram/notion/gsheets 的一致鏈(A)與一次填(C)**完全不依賴 D1**。
> 若 leo 看完仍要 D1(例如想未來在同一顆 DB 做 cred 審計/輪替記錄):那是**新需求**,需更新 2026-06-07 拍板。
> 屆時 credential **本體**仍建議 KV/Secrets Store(密文不需 SQL),D1 只放**非機密的 metadata**cred 名稱/服務/建立時間/last_verified),與 workflow/recipe 進 D1 同模式。**不要把密文搬進 D1 當主存。**
---
## 5. 與既有鐵律/SDD 的對齊
- **不刪 recipe、不禁 `acr creds push`**leo spec)。A 純補種子 + 型別欄位;C 是擴 `acr creds push` 輸入源,非拆除。
- 不新建 componentmindset §1 / rule 02)。
- credential 解密仍在 WASMrule 02 §2.2);KV→D1 只是密文來源從 `kv_get` 換成新 host function `cred_get`(屬 wasi-shimrule 02 §2.3 合法)。
- 薄殼(rule 07):`acr creds push` 讀 .env 仍只做「輸入解析 + client 加密 + 呼叫 API」,能力(store)在 API/D1,不在介面層。
- 設計權威:`auth-recipe.md` §六(telegram path 注入 line 70-71)、§七(kbdb 共用 line 150-151);
需求源 `docs/user_requirements/credential_parts.md`credential 長期規格)。