feat: 15 logic component Workers + cypher-executor auth/credentials routing

Component Workers:
- Deploys if_control, switch, filter, merge, try_catch, wait, set,
  array_ops, string_ops, number_ops, date_ops, validate_json,
  ai_transform_compile, ai_transform_run, foreach_control as
  independent Workers, backing cypher-executor's SVC_* service
  bindings (fast internal RPC for logic components).

cypher-executor routing:
- New routes: /auth (recipe resolution), /credentials (CRUD),
  /webhooks/named (user-friendly alias for cmp_/rec_ hashes).
- auth-recipe-seeds.ts: 20 pre-built platform auth recipes
  (Google Sheets, Gmail, Telegram, etc.) seeded into RECIPES KV.
- graph-executor + cypher-handlers + search-nodes updated for
  the new resolution chain.
- scripts/seed-auth-recipes.ts: one-shot tool to push seeds to KV.
- wrangler.toml: 15 SVC_* bindings wired to the new logic Workers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 17:40:02 +08:00
parent 6a3219e51b
commit 500d796573
92 changed files with 27237 additions and 72 deletions
+115
View File
@@ -123,3 +123,118 @@ export async function resolveRecipe(
// 直接用 canonical_id
return kv.get(`recipe:${id}`, 'json');
}
// ── Auth Recipe ────────────────────────────────────────────────────────────────
export type AuthPrimitive = 'static_key' | 'oauth2' | 'service_account' | 'mtls';
export interface SecretRequirement {
key: string; // CREDENTIALS_KV 的名稱(e.g. "notion_token"
label: string; // CLI/UI 顯示(e.g. "Internal Integration Token"
type?: 'string' | 'json_blob'; // default: string
help?: string;
help_url?: string;
optional?: boolean;
}
export interface AuthInjectSpec {
header?: Record<string, string>; // e.g. { Authorization: "Bearer {{secret.token}}" }
query?: Record<string, string>;
body?: Record<string, string>;
}
export interface AuthRecipeDefinition {
kind: 'auth_recipe';
service: string; // canonical_ide.g. "notion"
version: number;
primitive: AuthPrimitive;
base_url: string;
display_name?: string;
description?: string;
// service_account 專用
service_account_kind?: 'google_jwt';
token_exchange?: {
endpoint: string;
scopes: string[];
};
required_secrets: SecretRequirement[];
inject: AuthInjectSpec;
created_at: number;
updated_at: number;
}
/** 查 auth recipeKV key: auth_recipe:{service}*/
export async function resolveAuthRecipe(
service: string,
kv: KVNamespace,
): Promise<AuthRecipeDefinition | null> {
return kv.get(`auth_recipe:${service}`, 'json');
}
// POST /auth-recipes — 新增或更新 auth recipe
recipesRouter.post('/auth-recipes', async (c) => {
let body: Partial<AuthRecipeDefinition>;
try {
body = await c.req.json();
} catch {
return c.json({ success: false, error: 'request body 必須為 JSON' }, 400);
}
const service = (body.service ?? '').trim().toLowerCase();
if (!service) return c.json({ success: false, error: 'service 必填' }, 400);
if (!body.primitive) return c.json({ success: false, error: 'primitive 必填' }, 400);
if (!body.base_url) return c.json({ success: false, error: 'base_url 必填' }, 400);
if (!body.required_secrets?.length) return c.json({ success: false, error: 'required_secrets 必填' }, 400);
if (!body.inject) return c.json({ success: false, error: 'inject 必填' }, 400);
const now = Date.now();
const existing = await c.env.RECIPES.get(`auth_recipe:${service}`, 'json') as AuthRecipeDefinition | null;
const recipe: AuthRecipeDefinition = {
kind: 'auth_recipe',
service,
version: body.version ?? 1,
primitive: body.primitive,
base_url: body.base_url,
display_name: body.display_name,
description: body.description,
service_account_kind: body.service_account_kind,
token_exchange: body.token_exchange,
required_secrets: body.required_secrets,
inject: body.inject,
created_at: existing?.created_at ?? now,
updated_at: now,
};
await c.env.RECIPES.put(`auth_recipe:${service}`, JSON.stringify(recipe));
return c.json({ success: true, recipe });
});
// GET /auth-recipes — 列出所有 auth recipe
recipesRouter.get('/auth-recipes', async (c) => {
const list = await c.env.RECIPES.list({ prefix: 'auth_recipe:' });
const recipes = await Promise.all(
list.keys.map(k => c.env.RECIPES.get(k.name, 'json'))
);
return c.json({ success: true, recipes: recipes.filter(Boolean), count: recipes.length });
});
// GET /auth-recipes/:service — 讀取單一 auth recipe
recipesRouter.get('/auth-recipes/:service', async (c) => {
const service = c.req.param('service');
const recipe = await resolveAuthRecipe(service, c.env.RECIPES);
if (!recipe) return c.json({ success: false, error: `找不到 auth recipe: ${service}` }, 404);
return c.json({ success: true, recipe });
});
// DELETE /auth-recipes/:service — 刪除 auth recipe
recipesRouter.delete('/auth-recipes/:service', async (c) => {
const service = c.req.param('service');
const recipe = await resolveAuthRecipe(service, c.env.RECIPES);
if (!recipe) return c.json({ success: false, error: `找不到 auth recipe: ${service}` }, 404);
await c.env.RECIPES.delete(`auth_recipe:${service}`);
return c.json({ success: true, deleted: service });
});