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
@@ -50,6 +50,11 @@ type Input struct {
// Namesresolve_credentials action 用——要解密的 credential 名稱清單
// (用戶在 workflow node.data 寫 {{credential.NAME}} 時,graph-executor 收集後傳入)。
Names []string `json:"names,omitempty"`
// ResolvedSecretscredential-store 遷移 T6/T7 方案 AD19)——cypher-executor 已從
// 自己的 per-script secrets(新家)用 secret_get(D1 ref) 取到的明文,key = credential name。
// 有值就優先用,沒有才 fallback 舊路徑(kv_get {api_key}:cred:{name} + crypto_decrypt),
// 這個 fallback 就是雙讀(T7)。default(此欄空/nil)行為完全等於遷移前的舊碼。
ResolvedSecrets map[string]string `json:"resolved_secrets,omitempty"`
}
type SecretRequirement struct {
@@ -143,6 +148,15 @@ func main() {
if req.Optional {
continue
}
// T6 方案 Acypher 已從新家(per-script secrets)取到明文就直接用,不走 KV。
// input.ResolvedSecrets 為 nil 時此 map 讀取回 ("", false),行為與遷移前一致。
if v, ok := input.ResolvedSecrets[req.Key]; ok {
secrets[req.Key] = v
continue
}
// T7 雙讀 fallback:新家沒給 → 走舊路徑 kv_get + crypto_decrypt。
kvKey := input.APIKey + ":cred:" + req.Key
encJSON, s := kvGet(kvKey)
if s == 2 {
@@ -220,6 +234,14 @@ func handleResolveCredentials(input Input) {
if name == "" {
continue
}
// T6 方案 A:cypher 已從新家取到明文就直接用(同 authenticate 分支)。
if v, ok := input.ResolvedSecrets[name]; ok {
credentials[name] = v
continue
}
// T7 雙讀 fallback:新家沒給 → 舊路徑 kv_get + crypto_decrypt。
kvKey := input.APIKey + ":cred:" + name
encJSON, s := kvGet(kvKey)
if s == 2 {