# 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_id，6 個 API 零件 + 4 個 auth primitive）＋ `wasmWorkerUrl()` URL 慣例輔助函數；解析鏈新增為第 8 層（放在 `BUILTIN_API_RECIPES` fallback 之後，避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404；Phase 3 刪除 `BUILTIN_API_RECIPES` 後，API 零件會自然落到此層）。auth primitive 從此層進入。`tsc --noEmit` 通過。

---

## Phase 1：auth_static_key WASM（優先，涵蓋 80% 服務）

方案 B：WASM 自行讀 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 recipe（Basic Auth）→ 成功注入
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**（`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪）

---

## Phase 2：auth_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（純 Go，base64 decode + 去 header/footer）
  - 組 JWT header + payload（base64url），呼叫 `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 runner（HTTP 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 4：auth_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 7：auth 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 binding（HTTP URL）。
- 後來架構演化：用戶要的是 **recipe（資料，存 KV）+ 固定 primitive**，不是新零件。`acr recipe push` 進 KV **不 deploy**。用戶用「http_request primitive + 不同 recipe」打各種服務，永不新增 primitive。
- 結論：「用戶建 workflow 要 deploy」這個禁令前提**在 recipe 模型下不成立**。primitive（http_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 精確定義（官方 docs，2026-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 522（P0#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 走同路綁得上。

### 修法演進：A（service binding）→ 廢 → B（global_fetch_strictly_public flag）

**先做了 A（service binding）後評估廢棄，改用 B（flag）**——richblack 2026-06-06 拍板。

- **A（service 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——查證後子域同 zone（zone=註冊域名）照踩 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 確認 change，design §8）

> 根因：壓測 401 = workflow 寫 `{{credential.X}}` 但三條 template 展開路徑都不認此 namespace。
> 修法：auth_static_key 加 `resolve_credentials` action（WASM 解密），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` 是用戶面入口，與 authenticate（recipe 自動注入）並存不重疊
- Phase 0.6（host functions）+ 0.7（WASM 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
