# 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 改寫)|對應 issue:Arcrun#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 / gsheets(http_request + 固定設定)是打 API 的端點,**不刪**。改的是「credential 怎麼餵」,不是 recipe。 --- ## 0.5 Q1 — list 是動態讀 store 還是 hardcoded?(已核實 code,2026-06-29) **結論:recipe/auth-recipe 的「正規清單」是動態的;但 `acr parts` 是 hardcoded 且把 5 個 recipe 混進去 → 那 5 個之外的 pushed recipe 在 `acr parts` 看不到。** | 指令 | 讀哪裡 | 動態? | pushed recipe 會出現? | |---|---|---|---| | `acr recipe push` | `POST /recipes` → KV store(recipe.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(零件=WASM)hardcoded 是對的**:零件只能 PR merge 新增(mindset §4 人類閘門),固定、慢增,靜態清單反映真實。 - **但 `acr parts` 把 5 個 recipe 也 hardcode 進去**(parts.ts:197-261:gmail_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…=WASM,PR-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 + 實測 live,2026-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」長久存 → **此遷移從未完成**。 → B(KV→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` 補 telegram(inject.path bot_token)/ line_notify(header Bearer)/ kbdb(header Bearer),形態取自 prod。 - [x] `recipes.ts` `AuthInjectSpec` 補 `path?`(WASM/SDD §六早有、source 介面漏 → tsc 擋)。 - [ ] google_user(oauth2)暫不回灌:內嵌 client_secret 不可進 git + 介面無 oauth2 欄 → 留 Phase D。 - 部署後 leo21c 跑 `acr update`(/init/seed) → 23→26,telegram 即與 notion 同鏈可用。 - **不刪任何 recipe;不碰 `acr creds push`。** 純補種子 + 補型別欄位。 ### Phase B —(已翻案,見 §4.5)儲存:**KV 維持,不搬 D1**;高敏感可選補 Secrets Store > 早先版本寫「KV→D1 遷移」。§4.5 核實史實 + leo 自己 2026-06-07 拍板(credential 留 KV)後**翻案**: > credential=Redis-class(按 key 直接點查、密文不可 SQL 查),搬 D1 是淨負債。**B 改為:** 1. **低敏感 credential 維持 KV**(現狀 CREDENTIALS_KV,AES-GCM)。`acr creds list` 已可用(`GET /credentials` KV.list)。 2. **(可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Store**(credential_parts.md §8.2,KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。 3. **不新增 D1 credentials 表、不新增 `cred_get` host function、不做 KV→D1 migration**——這些原計畫取消(好處 KV 已有)。 4. 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets Store,D1 只放非機密 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 端加密 → 存進 D1(Phase 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/sendMessage`,body=`{chat_id,text}`。 這條鏈與 notion(header 注入)/gsheets(service_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` 只有 KBDB(entries/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_KV,MVP 至今未變) | | **kbdb-base SDD 拍板 2026-06-07(leo 親簽「Leo:同意」)** | **credential 留 KV**(「session、verdict、credential 留 KV=短期高頻純取用,類比 Redis;workflow/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 Store,KV 只存 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-off,mindset §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.2,KV 只存 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` 輸入源,非拆除。 - 不新建 component(mindset §1 / rule 02)。 - credential 解密仍在 WASM(rule 02 §2.2);KV→D1 只是密文來源從 `kv_get` 換成新 host function `cred_get`(屬 wasi-shim,rule 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 長期規格)。