/** * Auth Dispatcher * * 對需要認證的零件,在執行前 HTTP POST 到對應的 auth primitive Worker, * 取回 auth_headers / auth_query / auth_body 合併進節點 context。 * * 嚴格邊界(rule 02 §2.2): * - 本檔**不做**任何 credential 解密 / template 展開 / JWT 簽章 * - 那些全部在 auth primitive WASM 零件內執行(透過 host function `crypto_decrypt` 等) * - 本檔只做「查 recipe 決定走哪個 primitive Worker」+「HTTP fetch 取回注入結果」 * * 目前階段接上 `auth_static_key` + `auth_service_account` + `auth_oauth2`, * Phase 4 剩 `auth_mtls`(mTLS handshake 在 Worker runtime 層)。 * * 執行時機:graph-executor 在節點 runner 執行前呼叫,取回的 ctx 會: * 1. 先試本 dispatcher(命中才 return enriched ctx) * 2. 沒命中 fallback 到 `injectCredentials`(Phase 1.9 才刪除) */ import type { Bindings } from '../types'; import { resolveAuthRecipe, resolveRecipe } from '../routes/recipes'; import { wasmWorkerUrl } from '../lib/component-loader'; /** 對應 Phase 1-4 會部署的 auth primitive Worker */ const SUPPORTED_PRIMITIVES = new Set(['static_key', 'service_account', 'oauth2']); /** auth primitive 本身的 componentId(避免自引用) */ const AUTH_PRIMITIVE_IDS = new Set([ 'auth_static_key', 'auth_service_account', 'auth_oauth2', 'auth_mtls', ]); /** * 試著對零件做 auth 注入。 * - 命中(有對應 auth recipe 且 primitive 已支援)→ 回傳注入後的 ctx * - 未命中 → 回傳 null(呼叫端繼續跑舊路徑) */ export async function tryAuthDispatch( componentId: string, input: Record, env: Bindings, apiKey: string, ): Promise | null> { if (AUTH_PRIMITIVE_IDS.has(componentId)) { // auth primitive 本身不需要再做 auth return null; } // 決定 auth service name: // 1. 若 API recipe 宣告了 auth_service(例 recipe:kbdb_get → "kbdb")→ 用它, // 讓多個 recipe 共用同一把 auth_recipe(不必每個 action 複製 auth recipe)。 // 2. 否則 fallback 到把 componentId 當 service name(向後相容舊行為)。 let service = componentId; const apiRecipe = await resolveRecipe(componentId, env.RECIPES); if (apiRecipe?.auth_service) { service = apiRecipe.auth_service; } const recipe = await resolveAuthRecipe(service, env.RECIPES); if (!recipe) return null; if (!SUPPORTED_PRIMITIVES.has(recipe.primitive)) return null; // 走新路徑:HTTP POST 到對應 auth primitive Worker // 走 workers.dev 避開同 zone 死鎖(P0 #9) const primitiveUrl = wasmWorkerUrl(`auth_${recipe.primitive}`, env.WORKER_SUBDOMAIN); const res = await fetch(primitiveUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'authenticate', api_key: apiKey, service, }), }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error( `auth primitive "${recipe.primitive}" 回傳 ${res.status}: ${text.slice(0, 200)}`, ); } const result = await res.json().catch(() => null) as { success?: boolean; error?: string; auth_headers?: Record; auth_query?: Record; auth_body?: Record; auth_path?: Record; } | null; if (!result || result.success === false) { throw new Error( `auth primitive 失敗: ${result?.error ?? '未知錯誤'}`, ); } return { ...input, _auth_headers: result.auth_headers ?? {}, _auth_query: result.auth_query ?? {}, _auth_body: result.auth_body ?? {}, _auth_path: result.auth_path ?? {}, }; }