d2048e26a7
壓測階段 11:self-hosted 帳號 cypher 用 fetch(workers.dev) 打同帳號 auth worker 被 CF 子請求限制回 1042,service account token 換不到 → 表單寫不進 Google Sheets。token/解密鏈本身正常(直打 auth worker 回真 ya29)。 架構演化(richblack 2026-06-06 拍板):用戶產生的是 recipe(KV 資料,不 deploy), primitive 是平台固定基礎設施、用戶不新增 → 解除「auth primitive 禁 service binding」 舊禁令。service binding 是 CF 內部 RPC,繞開同 zone 522 + 同帳號 workers.dev 1042。 - wrangler.toml:加 SVC_AUTH_STATIC_KEY/SERVICE_ACCOUNT/OAUTH2(已部署者;mtls 未部署留註解) - auth-dispatcher.ts:binding 優先 svc.fetch(),無 binding fallback fetch(workers.dev) - types.ts:4 個 optional SVC_AUTH_* - deploy.ts 無需改:stripOfficialOnlyBindings 不碰 services,tier1 auth 先於 tier2 cypher - 已驗證 self-hosted(leo21c)13 邏輯零件 binding 實綁成功,auth binding 走同路 規範同步:rule 02 / 03 / CLAUDE.md / pre-bash-guard 例外。SDD: Phase 7。tsc exit 0。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
/**
|
||
* 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<string, keyof import('../types').Bindings> = {
|
||
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<string, unknown>,
|
||
env: Bindings,
|
||
apiKey: string,
|
||
): Promise<Record<string, unknown> | 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<string, string>;
|
||
auth_query?: Record<string, string>;
|
||
auth_body?: Record<string, string>;
|
||
auth_path?: Record<string, string>;
|
||
} | 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 ?? {},
|
||
};
|
||
}
|