feat(credentials): T6 讀取/注入接新家 + T7 雙讀 fallback (credential-store-migration 方案 A)

D19「擁有目錄不擁有內容物」遷移的讀取斷點。密文住 cypher per-script secrets、
D1 只存 secret_ref、auth worker 讀不到 cypher 的 secrets → cypher 先取值塞 payload。

WASM 端(T7 fallback 骨架,零行為改變可獨立部署驗):
- auth_static_key/main.go + auth_service_account/main.go:Input 加 resolved_secrets,
  解密處改「有 resolved 就用、沒有才 fallback 舊 kv_get+crypto_decrypt」。default 等於舊碼。
- tinygo build 兩支通過,copy 到 .component-builds/*/component.wasm。

TS 端(T6 主路徑):
- auth-dispatcher.ts 新增 resolveSecretsFromNewHome:查 D1 拿 secret_ref →
  secret_get(env[ref], T4) 取明文 → 組 map(取不到的 name 缺席,不放空字串) →
  更新 last_used_at。tryAuthDispatch + resolveCredentialRefs 都塞 resolved_secrets。
- rule 02 §2.2 對齊:只查 ref/取值/塞字串,不解密不展開模板不組 JWT。

驗證:cypher-executor + cli tsc exit 0;vitest 41/42(新增 auth-dispatcher.test.ts 6 案例全過,
剩 1 pre-existing 無關失敗)。待 leo21c 部署驗 WASM 端到端(不由本任務部署,acr update
硬綁 GitHub codeload=mistakes #23)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-07-05 13:44:26 +08:00
parent 55a47d7c18
commit ed2e42e007
7 changed files with 314 additions and 26 deletions
@@ -250,7 +250,34 @@ CLI 薄殼(rule 07):`acr creds list`(讀 D1 顯示)、`acr creds repla
正確)→ 清理:CF API `DELETE .../secrets/{ref}` + D1 `DELETE FROM credentials`,二次查詢確認
皆已清空。部署完成後**立刻 restore `wrangler.toml` 備份**git 追蹤版本無 leo21c 帳號專屬 id
殘留(已 grep 確認)。
- [] T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → `env[ref]` 取值(§2.5+ 更新 last_used。
- [x] T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → `env[ref]` 取值(§2.5+ 更新 last_used。
2026-07-05 本機(有 TinyGo 0.40.1)接手,方案 Aleo 已拍板〕**接續 2026-07-04 [cloud-worker]
揭露的正確落地方式**cypher 先取值塞 payload,因 auth worker 讀不到 cypher 的 secrets):
**WASM 端(T7 fallback 骨架,可獨立部署驗)**:
- `registry/components/auth_static_key/main.go`Input 加 `ResolvedSecrets map[string]string`
`authenticate` 的解密迴圈與 `handleResolveCredentials` 都改為「`input.ResolvedSecrets[key]`
有值就用,沒有才 fallback 舊 `kvGet`+`cryptoDecrypt`」。default(此欄 nil)行為完全等於遷移前舊碼。
- `registry/components/auth_service_account/main.go`SA JSON 取得同型(有 resolved 就用,沒有才舊路徑)。
- `tinygo build -target=wasi` 兩支皆通過(static_key 1.13MB / service_account 1.18MB,在 2MB 限制內),
copy 到 `.component-builds/{name}/component.wasm`。
**TS 端(T6 主路徑)**`cypher-executor/src/actions/auth-dispatcher.ts` 新增
`resolveSecretsFromNewHome(env, apiKey, names)`:查 D1 `credentials`api_key+name IN names)拿
`secret_ref` → `createArcrunHostFunctions(env, apiKey).secret_get(ref)`T4host 端 = `env[ref]`
取明文 → 組 `{name:value}` map,**取不到值的 name 缺席(不放空字串)** 讓 WASM fallbackT7)→
取到值的 name `UPDATE credentials SET last_used_at`。`tryAuthDispatch` 取 recipe 非 optional
`required_secrets[].key` 當 names、`resolveCredentialRefs` 用已收集的 names,兩者都把
`resolved_secrets` 塞進送 auth WASM 的 POST body。**rule 02 §2.2 對齊**:只查 ref/取值/塞字串,
不解密、不展開模板、不組 JWT(grep 確認無 `crypto.subtle`/`interpolate`/`{{secret.`/`BUILTIN_*`)。
**驗證**`cypher-executor` + `cli` `tsc --noEmit` 皆 exit 0`vitest run` 41/42(新增
`tests/auth-dispatcher.test.ts` 6 案例全過:D1 有 ref+新家有值→進 map/無 ref→缺席走 fallback
有 ref 但新家沒值 secret_get 回 null→缺席不放空字串/混合/空 names/取值後更新 last_used_at
剩 1 個 pre-existing 無關失敗 executor.test.ts「不存在」措辭,見 T8 記錄,非本次退步——
git baseline 35/36 → 41/42,只增不減)。
**待 leo21c 部署驗(本任務不部署,`acr update` 硬綁 GitHub codeload=mistakes #23,交總管/後續)**
WASM 端到端(resolved_secrets 命中新家的真實注入)在 vitest 測不到(JSPI import 限制,同 T4 記錄),
`secret_get` 的 pointer/memory-write 也待部署端到端涵蓋。TS 側 `resolveSecretsFromNewHome` 的
D1 查詢 + secret_get + last_used 更新已用真實 D1 binding + fake env(模擬 per-script secret 注入)
單元覆蓋。
2026-07-04 [cloud-worker] 卡在框架級工具缺口,非設計問題〕**T6 需要修改
`registry/components/auth_static_key/main.go`(與 `auth_service_account/main.go`**
這兩個 auth primitive 是獨立部署的 TinyGo WASM Worker,密文值現在住在
@@ -273,10 +300,17 @@ CLI 薄殼(rule 07):`acr creds list`(讀 D1 顯示)、`acr creds repla
決定是否要在雲端環境補裝工具鏈(例如允許 proxy 放行 github.com release 下載,或
改用其他 TinyGo 取得管道)。整案不硬繞(不改用「TS 直接組 header」這種違反 rule 02
的捷徑),留給有 TinyGo 環境的一方接手 main.go 改動 + 重新編譯部署。
- [] T7 雙讀 fallback(§4.1)。〔同上,隨 T6 main.go 改動一併落地——D1 查得到 secret_ref
走新路徑,查不到就是 WASM 原有的 `kv_get`+`crypto_decrypt` 分支,本來就會自然發生,
不需要額外的「fallback 判斷」程式碼,只要 T6 的 `resolved_secrets` 是「有給才用、沒給
就照舊」的設計即可。同樣卡在 T6 的 TinyGo 前置。〕
- [x] T7 雙讀 fallback(§4.1)。〔2026-07-05 本機,隨 T6 一併落地。〕如原設計預期:
**不需額外的「fallback 判斷」程式碼**——T6 的 `resolved_secrets` 就是「有給才用、沒給
就照舊 kv_get+crypto_decrypt」的設計(WASM 端 `input.ResolvedSecrets[key]` 的 `ok` 判斷即分流)。
雙讀在**兩層**自然成立:
(1) cypher 層——`resolveSecretsFromNewHome` 查 D1 查不到 ref(從未回填)或 `secret_get` 回 null
(新家還沒值)→ 該 name 不進 `resolved_secrets` map
(2) WASM 層——收到的 `resolved_secrets` 缺該 key → 走既有 `kvGet`+`cryptoDecrypt` 舊路徑。
新寫入(T5)一律新家;舊資料未回填(T8)前仍可讀(舊 KV 不刪,§4.3 回滾錨點不動)。
單元測試涵蓋「無 ref→缺席」「有 ref 但新家沒值 secret_get 回 null→缺席」兩條 fallback 分流
(見 T6 的 `tests/auth-dispatcher.test.ts`)。default(無 resolved_secrets 欄)行為 = 遷移前舊碼,
故 WASM 端此改動可獨立部署、零行為改變地驗「沒弄壞既有」。
- [x] T8 回填端點 `POST /credentials/migrate-to-workers-secrets`(§4.2,冪等可審)。
2026-07-04 [cloud-worker] 完成:`cypher-executor/src/routes/credentials.ts` 新增
端點,重用 `createArcrunHostFunctions(env, apiKey).crypto_decrypt`wasi-shim.ts