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:
@@ -62,6 +62,11 @@ type Input struct {
|
||||
APIKey string `json:"api_key"`
|
||||
Service string `json:"service"`
|
||||
Request json.RawMessage `json:"request,omitempty"`
|
||||
// ResolvedSecrets:credential-store 遷移 T6/T7 方案 A(D19)——cypher-executor 從自己的
|
||||
// per-script secrets(新家)用 secret_get(D1 ref) 取到的明文,key = credential name
|
||||
// (對 SA 而言就是 required_secrets[0].key)。有值優先用,沒有才 fallback 舊 KV+crypto_decrypt。
|
||||
// default(此欄空/nil)行為完全等於遷移前的舊碼。
|
||||
ResolvedSecrets map[string]string `json:"resolved_secrets,omitempty"`
|
||||
}
|
||||
|
||||
type SecretRequirement struct {
|
||||
@@ -176,28 +181,38 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 解密 service account JSON (慣例:required_secrets[0] 是 SA JSON)
|
||||
// 2. 取得 service account JSON 明文 (慣例:required_secrets[0] 是 SA JSON)
|
||||
saReq := recipe.RequiredSecrets[0]
|
||||
kvKey := input.APIKey + ":cred:" + saReq.Key
|
||||
encJSON, s := kvGet(kvKey)
|
||||
if s == 2 {
|
||||
writeError("缺少 credential: " + saReq.Key + " (" + saReq.Label + ")。修復: 編輯 credentials.yaml 後執行 acr creds push")
|
||||
return
|
||||
}
|
||||
if s != 0 {
|
||||
writeError("kv_get 失敗(credential " + saReq.Key + ")")
|
||||
return
|
||||
}
|
||||
|
||||
var rec EncryptedRecord
|
||||
if err := json.Unmarshal([]byte(encJSON), &rec); err != nil {
|
||||
writeError("credential " + saReq.Key + " 格式錯誤: " + err.Error())
|
||||
return
|
||||
}
|
||||
saJSONStr, ok := cryptoDecrypt(rec.Encrypted, rec.IV)
|
||||
if !ok {
|
||||
writeError("credential " + saReq.Key + " 解密失敗")
|
||||
return
|
||||
var saJSONStr string
|
||||
// T6 方案 A:cypher 已從新家(per-script secrets)取到明文就直接用,不走 KV。
|
||||
// input.ResolvedSecrets 為 nil 時此 map 讀取回 ("", false),行為與遷移前一致。
|
||||
if v, ok := input.ResolvedSecrets[saReq.Key]; ok {
|
||||
saJSONStr = v
|
||||
} else {
|
||||
// T7 雙讀 fallback:新家沒給 → 舊路徑 kv_get + crypto_decrypt。
|
||||
kvKey := input.APIKey + ":cred:" + saReq.Key
|
||||
encJSON, s := kvGet(kvKey)
|
||||
if s == 2 {
|
||||
writeError("缺少 credential: " + saReq.Key + " (" + saReq.Label + ")。修復: 編輯 credentials.yaml 後執行 acr creds push")
|
||||
return
|
||||
}
|
||||
if s != 0 {
|
||||
writeError("kv_get 失敗(credential " + saReq.Key + ")")
|
||||
return
|
||||
}
|
||||
|
||||
var rec EncryptedRecord
|
||||
if err := json.Unmarshal([]byte(encJSON), &rec); err != nil {
|
||||
writeError("credential " + saReq.Key + " 格式錯誤: " + err.Error())
|
||||
return
|
||||
}
|
||||
decrypted, ok := cryptoDecrypt(rec.Encrypted, rec.IV)
|
||||
if !ok {
|
||||
writeError("credential " + saReq.Key + " 解密失敗")
|
||||
return
|
||||
}
|
||||
saJSONStr = decrypted
|
||||
}
|
||||
|
||||
// 3. 解析 service account JSON
|
||||
|
||||
@@ -50,6 +50,11 @@ type Input struct {
|
||||
// Names:resolve_credentials action 用——要解密的 credential 名稱清單
|
||||
// (用戶在 workflow node.data 寫 {{credential.NAME}} 時,graph-executor 收集後傳入)。
|
||||
Names []string `json:"names,omitempty"`
|
||||
// ResolvedSecrets:credential-store 遷移 T6/T7 方案 A(D19)——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 方案 A:cypher 已從新家(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 {
|
||||
|
||||
Reference in New Issue
Block a user