Files
Arcrun/system-dev/docs/3-specs/arcrun/credential-primitives-wasm/tasks.md
T
uncle6me-web 5d00e71275 chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)
頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定,
Gitea private=除機敏值/build 產物/.github 外全 push。
解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。
機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:13:33 +08:00

238 lines
23 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.
# Implementation Tasks: Credential Primitives TS → WASM
**嚴格規範(richblack 2026-04-19**cypher-executor TS 不得實作任何 credential / auth / template / JWT / 解密邏輯。全部走 TinyGo WASM + host functions(方案 B)。
**封測狀態**:推遲(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。
---
## Phase 0:核心合併(u6u-core → arcrun
- [x] 0.1 把 `u6u-core/builtins/` 搬到 `arcrun/builtins/`
- [x] 0.2 確認 `arcrun/registry/components/` 21 個零件的 contract.yaml 完整(21/21
- [x] 0.3 刪除 `arcrun/credentials/` 整個目錄(重複,credential route 已在 cypher-executor
- [x] 0.4 更新 `arcrun/cypher-executor/wrangler.toml`:確認 CREDENTIALS_KV binding 存在
- [x] 0.5 刪除 `matrix/u6u-core/` 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代)
- [x] 0.6 在 `cypher-executor/src/lib/wasi-shim.ts` 新增 host functions
- `u6u.kv_get(keyPtr, keyLen, outPtr, outLenPtr) uint32` — 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES`,越權檢查 api_key
- `u6u.crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` — 用 `env.ENCRYPTION_KEY` + `crypto.subtle` AES-GCM 解密;key 不暴露給 WASM
- `u6u.crypto_sign_rs256(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32``crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`
- 2026-04-19 完成:wasi-shim.ts 新增 `createArcrunHostFunctions(env, apiKey)` factory,集中 AES-GCM 解密 + RSA sign + KV 前綴路由越權檢查。WASI imports 的 u6u namespace wiring 本來就已接好(只是當時沒有實作 factory)。typecheck 通過。
- [x] 0.7 在 `cypher-executor/src/lib/component-loader.ts` 新增 WASM runner 路徑:
- 所有 WASM 零件(含 auth primitive、API 零件、未來用戶自製)一律走 HTTP URL(`{canonical-id-kebab}.arcrun.dev`)到獨立 Worker
- **R2 動態注入路徑作廢**richblack 2026-04-19 確認:CF workerd 不支援以 R2 物件臨時 instantiate WASM;用戶自製零件同樣走「產生獨立 Worker」流程,不走 R2)
- cypher-executor 本身**不做** WASM instantiate,也不直接呼叫 `createArcrunHostFunctions`;那個 factory 是**零件 Worker 側**`.component-builds/{name}/src/index.ts`)用的,在 Phase 1 建立 auth_static_key Worker 時接上
- 2026-04-19 完成:`component-loader.ts` 新增 `WASM_HTTP_RUNNER_IDS`10 個 canonical_id6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
---
## Phase 1auth_static_key WASM(優先,涵蓋 80% 服務)
方案 BWASM 自行讀 KV + 解密,TS 不碰 plaintext。
- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄
- [x] 1.2 寫 `component.contract.yaml`input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`
- [x] 1.3 實作 `main.go`TinyGo):
- 宣告 host imports`kv_get` / `crypto_decrypt`static_key 不需要 http_request
- 從 stdin 讀 `{action, api_key, service}`
- `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"`
- 對每個 non-optional `recipe.required_secrets``kv_get("{api_key}:cred:{name}")``{encrypted, iv}``crypto_decrypt` → plaintext
- 展開 `{{secret.X}}` / `{{runtime.X}}` 模板於 `inject.header/query/body`;未知 key 展空字串(與 TS parity);其他 namespace 的 `{{...}}` 原樣保留
- 輸出 stdout JSON `{success, auth_headers, auth_query, auth_body, runtime}`
- [x] 1.4 `tinygo build -o auth_static_key.wasm -target=wasi main.go` — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內)
- [🔄] 1.5 建立 `.component-builds/auth_static_key/`(用 `component-worker-template`)並部署到 `auth-static-key.arcrun.dev`
- 2026-04-20 完成**建置**部分:`.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
- 方案 A:`src/index.ts` 直接 import `../../../cypher-executor/src/lib/wasi-shim``createWasiShim` + `createArcrunHostFunctions`(以 `ArcrunHostEnv` 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2)
- 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 `wrangler secret put`
- `wrangler deploy --dry-run` 通過(1192 KiB, 419 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
- [x] 1.6 建立 `auth-dispatcher.ts`(取代 `credential-injector.ts`):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 `_auth_headers` 進 ctx
- 2026-04-20 完成:`cypher-executor/src/actions/auth-dispatcher.ts` 新建,export `tryAuthDispatch(componentId, input, env, apiKey)`
- 流程:查 `resolveAuthRecipe` → primitive 在 `SUPPORTED_PRIMITIVES`(目前只有 `static_key`)→ fetch `wasmWorkerUrl('auth_static_key')` → 合併 `_auth_headers/_auth_query/_auth_body`
- 自引用防護:`AUTH_PRIMITIVE_IDS` set 排除 4 個 `auth_*` componentId
- `wasmWorkerUrl``component-loader.ts` export 出來共用
- `graph-executor.ts` 改為:先試 `tryAuthDispatch`(新路徑),沒命中 fallback 到舊 `injectCredentials`(Phase 1.9 刪)
- 檢查過 auth-dispatcher.ts 無 `crypto.subtle` / `interpolate` / `{{secret.` / hard-code API URL,符合 rule 02 §2.2
- `tsc --noEmit` 通過
- [ ] 1.7 端對端測試:openai recipe → 成功注入 `Authorization: Bearer <openai_key>`
- [ ] 1.8 端對端測試:twilio recipeBasic Auth)→ 成功注入
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪)
---
## Phase 2auth_service_account WASM
- [🔄] 2.1 建立 `arcrun/registry/components/auth_service_account/` 目錄
- [🔄] 2.2 寫 `component.contract.yaml`
- [🔄] 2.3 實作 `main.go`
- 從 stdin 讀 `{api_key, service}` + `kv_get` 拿 recipe + 解密 SA JSON
- 解析 SA JSON 取 `client_email` / `private_key`PEM
- PEM → PKCS8 bytes(純 Gobase64 decode + 去 header/footer
- 組 JWT header + payloadbase64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature
- 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token`
- 展開 `{{runtime.access_token}}` 模板
- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
- [x] 2.5 建立 `.component-builds/auth_service_account/` 並部署到 `auth-service-account.arcrun.dev`
- 2026-04-20 完成**建置**部分:`.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
- 方案 A:`src/index.ts` 重用 `createArcrunHostFunctions` 提供 kv_get/crypto_decrypt/crypto_sign_rs256,**額外加 `http_request` host function**(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token)
- 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 `wrangler secret put`
- `wrangler deploy --dry-run` 通過(1248 KiB, 440 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
- `auth-dispatcher.ts``SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑
- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
- [x] 2.7 **刪除 `lib/jwt-signer.ts` 整檔** — 2026-04-20 完成
- `cypher-executor/src/lib/jwt-signer.ts` 已刪除(RS256 JWT 邏輯移入 `auth_service_account.wasm`)
- `credential-injector.ts` 原 line 23 `import { exchangeGoogleJwt }` 移除
- `credential-injector.ts` 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)
- `cypher-executor` tsc --noEmit 通過
---
## Phase 3:清理 component-loader 的 TS 實作(全刪)
目標:`BUILTIN_API_RECIPES` 整段刪除,所有服務走 WASM runnerHTTP URL 路徑)。
- [x] 3.1 確認 `http_request.wasm` / `gmail.wasm` / `telegram.wasm` / `line_notify.wasm` / `google_sheets.wasm` 都在 `registry/components/` 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備
- [x] 3.2 確認上述零件 Worker 都已部署(`{name}.arcrun.dev` 可用) — 2026-04-20 完成**建置**部分
- 6 個 Worker 建置到位:`.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}`
- 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)`src/index.ts` 共用模板;cron 是純計算不註冊 host function
- 全部透過 `createWasiShim` 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界)
- 6 個 `wrangler deploy --dry-run` 全通過(~1.17 MB / ~413 KB gzip 每個);實際 `wrangler deploy` 留給 richblack 執行
- [x] 3.3 `component-loader.ts` 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成
- 原本第 7 層是 `BUILTIN_API_RECIPES` fallback、第 8 層是 `WASM_HTTP_RUNNER_IDS` (HTTP URL);兩層合併為第 7 層 `WASM_HTTP_RUNNER_IDS` 直接走 `makeHttpRunner(wasmWorkerUrl(id))`
- 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到)
- [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**`http_request` / `gmail` / `telegram` / `line_notify` / `google_sheets` / `cron` 的 TS 實作全刪) — 2026-04-20 完成
- `cypher-executor/src/lib/component-loader.ts` 原 line 253-326 `BUILTIN_API_RECIPES` 常數 + fallback lookup 全刪(約 80 行)
- 全域搜尋確認:`gmail.googleapis.com/...messages/send` / `api.telegram.org/bot.*sendMessage` / `sheets.googleapis.com/v4/spreadsheets` / `notify-api.line.me/api/notify` 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 `base_url` 是 recipe 資料欄位,不是 hard-coded API call)
- `cypher-executor` tsc --noEmit 通過
- [ ] 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信
- [ ] 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API
---
## Phase 4auth_oauth2 + auth_mtls WASM(封測後)
- [ ] 4.1 建立 `arcrun/registry/components/auth_oauth2/`
- [ ] 4.2 實作:`needs_refresh` / `refresh` / `authenticate` 三個 action
- [ ] 4.3 建立 `arcrun/registry/components/auth_mtls/`
- [ ] 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket
---
## Phase 5:封測啟動門檻 — 核心穩定驗證
**全部通過才能啟動封測**
- [ ] 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個)
- [ ] 5.2 `cypher-executor/src/actions/credential-injector.ts` **不存在**
- [ ] 5.3 `cypher-executor/src/lib/jwt-signer.ts` **不存在**
- [ ] 5.4 `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`
- [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts``crypto_decrypt` host function
- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts``crypto_sign_rs256` host function
- [ ] 5.7 `cypher-executor/src/` 全域搜尋 `interpolate` 回傳 0 筆(template 展開全在 WASM
- [ ] 5.8 全域搜尋 `{{secret\.` / `{{runtime\.` 在 TS 檔案中回傳 0 筆
---
## Phase 6:通用 CI/CD deploy workflow
**背景**(2026-04-20 richblack 決定):現 `.github/workflows/deploy.yml` 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為**通用掃描式 workflow**:任何含 `wrangler.toml` 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。
**關鍵決策**:
- 零件 `.wasm` 由 CI build(不 commit):`registry/components/{name}/main.go` 改動時才重 build,用 timestamp / content hash 判斷
- `.component-builds/{name}/component.wasm` 由 CI 從 `registry/components/{name}/{name}.wasm` 複製產生(deploy 前一步)
- 統一用 pnpm(`.component-builds/*` 本來就是;順勢把 cypher-executor 的 `package-lock.json` 砍了)
- runtime secret(`ENCRYPTION_KEY`)不進 CI,由 richblack 一次性 `wrangler secret put`
- registry Worker 的 `wrangler.toml` 現階段不改(職責是合約管理,與封測無關;`sandboxAcceptance.ts` 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時)
### Tasks
- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm`
- [x] 6.3 diff-aware:push 到 main 時比對 `github.event.before..github.sha`,只 deploy 有 diff 的 Worker(含 `registry/components/{name}/` 連動 `.component-builds/{name}/`);`workflow_dispatch` 提供 `force_all` + `only` 選項
- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯)
- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自)
- [x] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成
- 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success
- 過程中修兩輪:先修 `setup-node``cache: 'pnpm'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml
- ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key``arcrun-auth-service-account``arcrun-cypher-executor`(不顯示內容)
- [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)
---
## Phase 7auth primitive 改走 service binding(解 self-hosted CF 1042
> 2026-06-06 richblack 拍板。來源:壓測報告階段 11`test_arcrun/docs/壓測報告.md §11`)。
**背景(架構演化)**
- 當初設想「用戶對每個服務建**零件**」→ 零件用 service binding 要 redeploy → 用戶建 workflow 要 redeploy(不可接受)→ 故禁 service binding、改 cypher bindingHTTP URL)。
- 後來架構演化:用戶要的是 **recipe(資料,存 KV+ 固定 primitive**,不是新零件。`acr recipe push` 進 KV **不 deploy**。用戶用「http_request primitive + 不同 recipe」打各種服務,永不新增 primitive。
- 結論:「用戶建 workflow 要 deploy」這個禁令前提**在 recipe 模型下不成立**。primitivehttp_request + 4 個 auth)是**平台固定基礎設施**,和 13 個邏輯零件同類,給它 service binding 的 deploy 成本只在平台部署時付一次,**不落用戶身上**。
**根因(壓測階段 11,唯讀證據)**
- `auth-dispatcher.ts:67-68``fetch(wasmWorkerUrl('auth_service_account', WORKER_SUBDOMAIN))` 打 workers.dev → CF 回 **404 + "error code: 1042"**CF 邊緣頁,非 auth worker 回應)。
- **1042 精確定義(官方 docs2026-06-06 查證)**:「Worker tried to fetch from another Worker **on the same zone**, only supported when `global_fetch_strictly_public` flag is used」。關鍵是 **same zone**
- **self-hosted 踩**cypher`arcrun-cypher-executor.{sub}.workers.dev`)與 auth worker`arcrun-auth-*.{sub}.workers.dev`**同在 `{sub}.workers.dev` zone** → 同 zone fetch → 1042。
- **官方不踩**:官方 cypher 在自訂域 `cypher.arcrun.dev`,打 auth 的 `*.uncle6-me.workers.dev` 是**跨 zone**,不觸發 same-zone 限制(非「官方有 flag」——官方 wrangler.toml 只有 `nodejs_compat`,無 `global_fetch_strictly_public`)。
- 這是 P0#9「同 zone 522」的**另一形態**:之前解的是 `*.arcrun.dev` 同 zone,沒解到 `*.workers.dev` 同 zone。service binding 是內部 RPC 不經 zone → 徹底免疫(13 邏輯零件正是走 binding 才不踩)。
- 直接從外部打 `arcrun-auth-service-account.leo21c.workers.dev` → 200 + 真 `ya29...` token → **JWT/解密/token 換取鏈本身全正常**,唯一卡點是 cypher 打不到自己帳號的 auth worker。
- service binding 是 CF 內部 RPC,不經公網 → 同時繞開**同 zone 522P0#9**與**同帳號 workers.dev 子請求 1042**,比 workers.dev fetch 乾淨(13 邏輯零件正是用 binding 才不踩)。
**技術前提(已驗證 ✅)**
- CF API 查 leo21c(全新自架帳號)的 `arcrun-cypher-executor` settings → **實綁 13 個 service binding**SVC_SET→arcrun-set …)。證明 `deploy.ts` 的 binding 注入在 self-hosted 生效,auth binding 走同路綁得上。
### 修法演進:Aservice binding)→ 廢 → Bglobal_fetch_strictly_public flag
**先做了 Aservice binding)後評估廢棄,改用 Bflag)**——richblack 2026-06-06 拍板。
- **Aservice binding)為何廢**service binding 靜態,加/改要重 deploy cypher(官方 docs 證實)。richblack 判定不夠乾淨,且有更簡單的 B。
- **B`global_fetch_strictly_public` flag)為何對**:官方 docs——此 flag 讓 `fetch()` 走公網「前門」,**same-zone fetch 也能通**。cypher wrangler.toml 加一行即解,**用戶無感、不用域名、不用重 deploy、不動 service binding 禁令**。
- **B 安全(查證官方 docs**:唯一副作用是「Worker fetch 自己 hostname 會 self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
- **B 官方/self-host 共用**:官方 cypher 本就跨 zone,加 flag 行為不變;self-host 同 zone 被修好。同一份 toml 兩邊通用。
- **評估但廢的他案**arcrun.dev 子域給 self-host cypher——查證後子域同 zonezone=註冊域名)照踩 1042,且跨帳號 route 落地等於 PaaS 轉向,廢。
### Tasks
- [x] 7.1 規範:`02-forbidden §3.1` / `03` / `CLAUDE.md` 鐵律4 / `pre-bash-guard 3.1`——**禁令維持原狀(不解禁 service binding**,僅加註「same-zone 1042 用 flag 解」。(先前一度改成「平台 primitive 例外允許 binding」,因改用 B 已**全部還原**)
- [x] 7.2 ~~hook 解禁~~**還原**`pre-bash-guard.sh` 規則 3.1 回原本「禁止新增 service binding」(B 不需解禁)
- [x] 7.3 `cypher-executor/wrangler.toml``compatibility_flags``global_fetch_strictly_public`(+ 註解病因/安全)。**移除先前加的 SVC_AUTH_***grep SVC_AUTH=0
- [x] 7.4 `auth-dispatcher.ts` / `types.ts`:**還原**到 A 之前(移除 binding 優先邏輯與 SVC_AUTH_* 型別,回單純 `fetch(workers.dev)`——flag 讓它同 zone 也通);tsc exit 0
- [x] 7.5 `cli/src/lib/deploy.ts`**無需改**——flag 在 cypher wrangler.toml`stripOfficialOnlyBindings()` 不碰 `compatibility_flags`self-hosted 部署自動帶上
- [ ] 7.6 驗收(客觀證據):全新自架帳號 `POST /webhooks/named/{ns}/{wf}/trigger``append_row` 的 auth 注入回 200、真的寫進 Google Sheets(第一個端到端驗 self-hosted auth 鏈的測試);`tsc --noEmit` exit 0
- [x] 7.7 design.md 同步(§修改 auth-dispatcher 補「binding 優先、fetch fallback」+ 架構演化 + 1042 根因):§修改 的 auth-dispatcher 補「binding 優先、fetch fallback」;記架構演化(recipe 取代零件 → primitive 可 binding
---
## Phase 8`{{credential.X}}` 用戶面注入語法(2026-06-10 richblack 確認 changedesign §8
> 根因:壓測 401 = workflow 寫 `{{credential.X}}` 但三條 template 展開路徑都不認此 namespace。
> 修法:auth_static_key 加 `resolve_credentials` actionWASM 解密),graph-executor 偵測+回填(不解密)。
- [x] 8.1 `registry/components/auth_static_key/main.go`input 加 `Names []string`main 在 `service`
必填檢查**之前**分流 `action == "resolve_credentials"` → 走新 `handleResolveCredentials`(不查 recipe、
不要求 service),每個 name `kvGet("{api_key}:cred:{name}")` + `cryptoDecrypt` → 回
`{success, credentials:{name: plaintext}}`;缺 name → success:false + error 指明(不假綠)
- [x] 8.2 `tinygo build -target=wasi -o auth_static_key.wasm main.go` 編譯通過(1.1MB+ copy 到
`.component-builds/auth_static_key/component.wasm`
- [x] 8.3 `cypher-executor/src/graph-executor.ts`node.data interpolate 後、node 執行前,掃描殘留
`{{credential.NAME}}` → 有則 POST `wasmWorkerUrl('auth_static_key')`
`{action:"resolve_credentials", api_key, names}` → 回填 node.data。**不解密、不碰 ENCRYPTION_KEY**rule 02 §2.2
實作放 `auth-dispatcher.ts``resolveCredentialRefs`(遞迴掃描+回填,無 {{credential.}} 則零開銷不打 WASM),graph-executor 只呼叫
- [x] 8.4 `tsc --noEmit` exit 0 + grep 確認 graph-executor / auth-dispatcher 無 crypto.subtle / {{secret. / ENCRYPTION_KEY 業務邏輯
- [x] 8.5 端對端驗收:機制已實證打通(2026-06-13 leo21c 壓測)。實際走 **Notion** 而非 OpenAI 路徑——
node.data `{{credential.notion_token}}` → 注入解密後 token → 真讀到 Notion Recipes 資料
(「蕃茄蘑菇燉雞」+ iCook 連結),同時 401 假綠根治全鏈驗證(host fn error envelope →
零件 parsed["error"] → cypher isFailure()401 回 success:false)。客觀證據:真服務 2xx + 真 body。
8.5 原訂 OpenAI 為驗收服務,實際以 Notion 達成同等證據;機制與服務無關,故視為驗收完成)
- [x] 8.6 design.md / status.md / BACKLOG.md 同步(§8 設計 + 8.1-8.4 done 標記)
---
## Notes
- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
- Phase 8 的 `resolve_credentials` 是用戶面入口,與 authenticaterecipe 自動注入)並存不重疊
- Phase 0.6host functions+ 0.7WASM runner)是 Phase 1-3 的硬前置,必須先做
- 若 TinyGo `encoding/base64` 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 `base64URLEncode`
- `auth_mtls` 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用
- **每個 auth primitive WASM 都是獨立部署的 Worker**(透過 `component-worker-template/`),不是從 R2 動態載入
- Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding