/** * 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'; import type { ServiceBinding } from '../types'; /** 對應 Phase 1-4 會部署的 auth primitive Worker */ const SUPPORTED_PRIMITIVES = new Set(['static_key', 'service_account', 'oauth2']); /** * primitive 名 → service binding key(Phase 7,2026-06-06)。 * 比照 component-loader 的邏輯零件:有 binding 走 CF 內部 RPC(繞開同 zone 522 + 同帳號 workers.dev 1042), * 無 binding(如 self-hosted 未綁、或 mtls 未部署)fallback 到 fetch(workers.dev)。 */ const AUTH_BINDING_MAP: Record = { static_key: 'SVC_AUTH_STATIC_KEY', service_account: 'SVC_AUTH_SERVICE_ACCOUNT', oauth2: 'SVC_AUTH_OAUTH2', mtls: 'SVC_AUTH_MTLS', }; /** 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; // 呼叫對應 auth primitive Worker(Phase 7,2026-06-06): // binding 優先(CF 內部 RPC,繞開同 zone 522 + 同帳號 workers.dev 子請求 1042,壓測階段 11), // 無 binding(self-hosted 未綁 / mtls 未部署)fallback 到 fetch(workers.dev)。比照 component-loader makeLogicRunner。 const reqInit = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'authenticate', api_key: apiKey, service }), }; const bindingKey = AUTH_BINDING_MAP[recipe.primitive]; const svc = bindingKey ? (env[bindingKey] as ServiceBinding | undefined) : undefined; let res: Response; if (svc) { // service binding:用任意 URL,CF 內部 RPC 直送目標 Worker(不經公網) res = await svc.fetch(new Request('https://auth-primitive/', reqInit)); } else { // fallback:公網 workers.dev(自架未綁 binding / 開發環境 / mtls) const primitiveUrl = wasmWorkerUrl(`auth_${recipe.primitive}`, env.WORKER_SUBDOMAIN); res = await fetch(primitiveUrl, reqInit); } 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 ?? {}, }; }