Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
Auth Recipe System — SDD
文件類型:SDD(Software Design Document) 建立:2026-04-19 狀態:實作中
一、目標
封測前完成,讓封測者碰到「我要連 X 服務」都有辦法,而不是「還沒做」。
精神:http_request 是容器零件,auth recipe 是「如何對這個服務認證」的設定層,兩者分離。新增一個服務 = 寫一份 YAML,不需要改程式碼、不需要重新部署 Worker。
二、三層模型
Layer 3: Auth Recipe (YAML/JSON in RECIPES KV)
公共,描述「如何對某服務認證」
key: auth_recipe:{service}
例: auth_recipe:notion, auth_recipe:slack
↓ 引用
Layer 2: Auth Primitive (TypeScript in Worker)
四個通用認證邏輯:static_key | oauth2 | service_account | mtls
封測只做 static_key 和 service_account (Google JWT)
↑ 使用
Layer 1: Tenant Secret (CREDENTIALS_KV)
每個 tenant 自己的加密 credential
key: {api_key}:cred:{name}
三、Auth Recipe Schema
interface AuthRecipeDefinition {
kind: 'auth_recipe'; // 區別 RecipeDefinition 用
service: string; // canonical_id, e.g. "notion"
version: number;
primitive: 'static_key' | 'oauth2' | 'service_account' | 'mtls';
base_url: string;
display_name?: string;
description?: string;
// service_account 用
service_account_kind?: 'google_jwt';
token_exchange?: {
endpoint: string; // e.g. https://oauth2.googleapis.com/token
scopes: string[];
};
required_secrets: Array<{
key: string; // CREDENTIALS_KV 的名稱
label: string; // UI/CLI 顯示
type?: 'string' | 'json_blob'; // default: string
help?: string;
help_url?: string;
}>;
inject: {
header?: Record<string, string>; // "Authorization": "Bearer {{secret.token}}"
query?: Record<string, string>;
body?: Record<string, string>;
// path:注入 endpoint URL path 的 secret(2026-05-29 加)。
// 解 telegram 類「token 在 URL path」(/bot{token}/)—— header/query/body 都不適用。
// key = 模板變數名,API recipe 的 endpoint 用 {{auth.K}} 引用。
// 例:auth_recipe:telegram inject.path = { bot_token: "{{secret.telegram_bot_token}}" }
// recipe:telegram_send endpoint = "https://api.telegram.org/bot{{auth.bot_token}}/sendMessage"
path?: Record<string, string>;
};
created_at: number;
updated_at: number;
}
Template 語法:
{{secret.KEY}}→ 從 tenant 的 CREDENTIALS_KV 解密取值{{runtime.access_token}}→ service_account JWT exchange 後取得的短期 token
四、KV 儲存
沿用現有 RECIPES KV namespace,不新增 binding。
auth_recipe:{service} → AuthRecipeDefinition JSON
與現有 recipe:{id} / idx:{hash} 的 key 不衝突。
五、執行流程
5.1 static_key(涵蓋 ~80% 服務)
trigger → graph-executor
→ injectCredentials(componentId, input, env, apiKey)
→ resolveAuthRecipe("notion", RECIPES KV)
→ 取得 required_secrets: [{key: "notion_token", ...}]
→ 從 CREDENTIALS_KV 讀 "{api_key}:cred:notion_token"
→ AES-GCM 解密
→ 展開 inject.header templates ({{secret.notion_token}} → 實際值)
→ 注入 _auth_headers, _auth_query, _auth_body 到 input
→ makeAuthRecipeRunner(recipe)
→ 合併 _auth_headers 到 fetch headers
→ 呼叫 recipe.base_url + input._path
→ 回傳結果
5.2 service_account(Google 家族)
injectCredentials
→ resolveAuthRecipe("google_sheets_sa", RECIPES KV)
→ 解密 service_account_json (JSON blob)
→ signGoogleJwt(serviceAccountJson, scopes) via crypto.subtle (RSASSA-PKCS1-v1_5 + SHA-256)
→ POST token_exchange.endpoint → 取得 access_token
→ 展開 inject.header: { Authorization: "Bearer {{runtime.access_token}}" }
→ 注入 _auth_headers
六、Context key 慣例
注入後的認證資訊以 _auth_ 前綴攜帶,不污染業務欄位:
| Key | 說明 |
|---|---|
_auth_headers |
Record<string, string> — 要合併進 fetch headers |
_auth_query |
Record<string, string> — 要附加到 URL query string |
_auth_body |
Record<string, string> — 要合併進 request body |
_auth_path |
Record<string, string> — endpoint URL path 用(2026-05-29 加)。makeRecipeRunner 的 endpoint interpolate 用 {{auth.K}} 從這裡取值 |
makeAuthRecipeRunner / makeRecipeRunner 在發出 fetch 前讀取這些 _auth_* 欄位,
之後從 auto-body 中剔除所有 _ 前綴欄位(不洩漏給下游)。
七、API recipe 的 auth_service(多 recipe 共用一把 auth,2026-05-29 加)
RecipeDefinition 加 auth_service?: string 欄位:API recipe 自報它屬於哪個服務,
auth-dispatcher 用它查 auth_recipe:{auth_service},而非假設 componentId == service name。
- 讓多個 recipe 共用同一把 auth:
recipe:kbdb_get/kbdb_create_block都設auth_service: "kbdb"→ 共用唯一的auth_recipe:kbdb,加新 action 不必複製 auth recipe。 - auth-dispatcher 解析順序:先查
recipe:{componentId}拿auth_service,有就用它; 沒有則 fallback 把 componentId 當 service name(向後相容舊行為)。 - 這是「服務身分標籤」非「許可清單」:auth_recipe 只定義「怎麼認證」,不含「誰准用」。 授權由發 API key 的服務裁決,arcrun 不做內部授權判斷(見 DECISIONS.md「arcrun 不做授權判斷」)。
七、向後相容
- 現有
BUILTIN_API_RECIPES(gmail, google_sheets, telegram, line_notify)不動 - 現有
BUILTIN_CREDENTIALS_MAP不動 - auth recipe 解析在 component-loader step 5.5(新增),在 step 6 KV recipe 和 step 7 builtin 之前
- 若
auth_recipe:{service}不存在 → 繼續往下走,行為與現在完全相同
八、新增/修改的檔案
| 檔案 | 類型 | 說明 |
|---|---|---|
cypher-executor/src/routes/recipes.ts |
修改 | 加 AuthRecipeDefinition 型別、resolveAuthRecipe、/auth-recipes CRUD routes |
cypher-executor/src/actions/credential-injector.ts |
修改 | 加 auth recipe 分支:static_key + service_account |
cypher-executor/src/lib/jwt-signer.ts |
新增 | Google JWT signing via crypto.subtle |
cypher-executor/src/lib/component-loader.ts |
修改 | step 5.5 auth recipe lookup + makeAuthRecipeRunner |
cypher-executor/src/lib/auth-recipe-seeds.ts |
新增 | 20 個常用服務的 auth recipe 定義 |
cli/src/commands/auth-recipe.ts |
新增 | acr auth-recipe list/info/scaffold |
cli/src/commands/parts.ts |
修改 | cmdPartsScaffold fallback 到 auth recipe |
cli/src/index.ts |
修改 | 註冊 auth-recipe 指令 |
九、封測前預計的 Auth Recipe 清單(20 個)
static_key 類(~80% 服務)
| service | 認證方式 | credential key |
|---|---|---|
notion |
Bearer token (header) | notion_token |
slack |
Bot Token (Bearer) | slack_bot_token |
github |
PAT (Bearer) | github_token |
openai |
API key (Bearer) | openai_api_key |
anthropic |
API key (x-api-key) | anthropic_api_key |
airtable |
PAT (Bearer) | airtable_token |
discord |
Bot token ("Bot TOKEN") | discord_bot_token |
stripe |
Secret key (Bearer) | stripe_secret_key |
twilio |
AccountSid + AuthToken (Basic Auth) | twilio_account_sid, twilio_auth_token |
sendgrid |
API key (Bearer) | sendgrid_api_key |
hubspot |
Private App token (Bearer) | hubspot_token |
linear |
API key (Bearer) | linear_api_key |
shopify |
Admin API token (X-Shopify-Access-Token) | shopify_access_token |
resend |
API key (Bearer) | resend_api_key |
supabase |
Service role key (Bearer + apikey) | supabase_service_key |
typeform |
PAT (Bearer) | typeform_token |
jira |
API token + email (Basic Auth) | jira_api_token, jira_email |
service_account 類(Google 家族,JWT signing)
| service | scopes | credential key |
|---|---|---|
google_sheets_sa |
spreadsheets | google_service_account |
google_gmail_sa |
gmail.send | google_service_account |
google_drive_sa |
drive | google_service_account |
注意:三個 Google 服務可共用同一個
google_service_accountcredential,只是 scope 不同。
十、實作進度
Server (cypher-executor)
AuthRecipeDefinition型別 +resolveAuthRecipe/auth-recipesCRUD routesinjectFromAuthRecipe— static_key primitivelib/jwt-signer.ts— Google JWT via crypto.subtleinjectFromAuthRecipe— service_account primitivemakeAuthRecipeRunnerin component-loader- step 5.5 in createComponentLoader
- auth-recipe-seeds.ts (20 services)
- seed script / deploy seeds to KV(2026-04-19 全部 ✅)
CLI (arcrun)
commands/auth-recipe.ts— list / info / scaffold- 更新
commands/parts.ts— scaffold fallback - 更新
index.ts— 註冊指令 - 版本升 1.1.0
- npm publish(arcrun@1.1.0)
驗證
- notion (static_key) 端對端
- google_sheets_sa (service_account) 端對端
- 舊有 google_sheets builtin 向後相容確認
十一、長期演進:TinyGo WASM Primitive(封測後)
參考:
docs/user_requirements/arcrun/credential_parts.md
目前封測版:Layer 2 primitive 邏輯在 cypher-executor TypeScript 中實作(credential-injector.ts)。
長期目標:四個 primitive 各自編譯為獨立 TinyGo WASM,取代現有 TS 實作:
arcrun/registry/components/auth_static_key/ ← TinyGo WASM
arcrun/registry/components/auth_oauth2/ ← TinyGo WASM
arcrun/registry/components/auth_service_account/ ← TinyGo WASM
arcrun/registry/components/auth_mtls/ ← TinyGo WASM
每個 primitive 實作統一 interface(Authenticate / NeedsRefresh / Refresh / Test)。
切換時 cypher-executor 的 injectFromAuthRecipe 改為呼叫對應 WASM,邏輯不變。
何時做:封測驗證完成、TinyGo crypto 支援確認後(特別是 RS256/ES256 JWT signing)。 在此之前,不建立任何 TypeScript SDK 或 Python SDK 來包裝 credential 邏輯。
禁止的做法
- ❌ 建立
js-sdk/、python-sdk/包裝 credential 加解密 - ❌ 在 client 端重實作 AES-GCM encrypt/decrypt
- ❌ 用 TypeScript 重寫已計劃用 TinyGo 實作的 primitive 邏輯