Files
Arcrun/cypher-executor/src/index.ts
T
uncle6me-web 07cc7f51b5 fix(cypher): 同一台實例的 portal 一律自動放行,不再依賴 UI_ORIGINS 被注入
2026-08-08 事故根因:leo 的 youlin 實例登入整個斷掉,瀏覽器實證
  blocked by CORS policy: No 'Access-Control-Allow-Origin' header
真因=該台 UI_ORIGINS 沒被設。

同一天發生兩次同款:這些變數只有安裝器會注入,任何手動 wrangler deploy
就會漏掉,而漏掉時系統看起來完全正常(worker 上線、200、版本號對),
只有真人點下去才會發現。leo:「這麼危險的問題已經發生 2 次,不可以再有一次。」

⇒ 治法不是「記得注入」,是讓它不需要被注入:portal 與 cypher 是同一個
workers.dev 子網域下的兄弟,位址推導得出來。少一個必須注入的變數,
就少一個會被漏掉的東西。UI_ORIGINS 仍有效(自訂網域用),只是不再是
「登得進去」的前提。

對照:改動前後 tsc 錯誤數同為 7(皆為既有、不在本檔)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:47:32 +08:00

108 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// arcrun cypher-executor Worker — AI 工作流執行引擎
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import type { ExecutionContext } from '@cloudflare/workers-types';
import type { Bindings } from './types';
import { handleScheduled } from './scheduled';
import { healthRouter } from './routes/health';
import { executeRouter } from './routes/execute';
import { cypherRouter } from './routes/cypher';
import { validateRouter } from './routes/validate';
import { docsRouter } from './routes/docs';
import { webhooksRouter } from './routes/webhooks';
import { webhooksCrudRouter } from './routes/webhooks-crud';
import { webhooksListRouter } from './routes/webhooks-list';
import { recipesRouter } from './routes/recipes';
import { credentialsRouter } from './routes/credentials';
import { webhooksNamedRouter } from './routes/webhooks-named';
import { authRouter } from './routes/auth';
import { resumeRouter } from './routes/resume';
import { executionsRouter } from './routes/executions';
import { initSeedRouter } from './routes/init-seed';
import { kbdbProxyRouter } from './routes/kbdb-proxy';
import { consoleAuthRouter } from './routes/console-auth';
import { consoleDashboardRouter } from './routes/console-dashboard';
import { portalRouter } from './routes/portal';
import { portalDataRouter } from './routes/portal-data';
const app = new Hono<{ Bindings: Bindings }>();
// 全域 CORS(允許 arcrun.dev landing page 帶 credentials 存取)
//
// 2026-07-21 cypher-ui-splitconsole/portal UI 搬到 Cloudflare Pages 後,前端與本 API
// **不再同源**,故 UI 站的 origin 必須進白名單,否則所有 fetch 會被瀏覽器擋。
// 允許來源=上面兩個 landing + UI_ORIGINSwrangler.toml [vars] 逗號分隔,實例自填
// 自己的 Pages 網域,如 https://arcrun-console-ui.pages.dev)。
// 刻意不用萬用字元:credentials:true 與 `*` 在 CORS 規格上互斥,且 Authorization/
// X-Arcrun-API-Key 是憑證等級標頭,開全域等於誰都能代打。
const STATIC_ORIGINS = ['https://arcrun.dev', 'https://www.arcrun.dev'];
app.use('*', cors({
origin: (origin, c) => {
// ⚠️ 非瀏覽器請求(CLIcurlMCP)沒有 Origin 標頭 → origin 是空字串/undefined。
// 此時必須原樣放行,不能回 null——回 null 會讓 Hono cors 中介層在後續處理拋錯,
// 表現為所有 CLI 部署一律 5002026-07-21 實撞:acr push 全掛,對照組亦然)。
if (!origin) return origin;
let extra: string[] = [];
try {
extra = String((c.env as Record<string, unknown>).UI_ORIGINS || '')
.split(',').map((s: string) => s.trim()).filter(Boolean);
} catch { /* UI_ORIGINS 未設定=只用靜態白名單 */ }
// 🔴 2026-08-08 事故根因修復:**同一台實例的 portal 一律自動放行,不再依賴注入**。
//
// 那天發生什麼:leo 的 youlin 實例 portal 整個不能用——先是畫面頂端紅字
// 「設定檔沒載入(config.js)」(UI worker 缺 WORKER_SUBDOMAIN),修好之後**登入仍然失敗**。
// 瀏覽器 console 實證:
// Access to fetch at '…/portal/login' … blocked by CORS policy:
// No 'Access-Control-Allow-Origin' header is present
// 真因=這台的 `UI_ORIGINS` 沒被設。
//
// 兩次同一個病:**這些變數只有安裝器那條路會注入,任何人手動 `wrangler deploy` 就會漏掉——
// 而漏掉時系統看起來完全正常**(worker 上線、HTTP 200、版本號還是對的),
// 只有真人點下去才會發現。leo:「這麼危險的問題已經發生 2 次,不可以再有一次。」
//
// ⇒ 治法不是「記得要注入」,是**讓它不需要被注入**:
// portal 與本 worker 是同一個 workers.dev 子網域下的兄弟,位址推導得出來。
// **少一個必須注入的變數,就少一個會被漏掉的東西。**
// `UI_ORIGINS` 仍然有效(自訂網域/額外前端還是靠它),只是不再是「登得進去」的前提。
const sub = String((c.env as Record<string, unknown>).WORKER_SUBDOMAIN || '').trim();
const sibling = sub ? [`https://arcrun-rag-ui.${sub}.workers.dev`] : [];
return [...STATIC_ORIGINS, ...sibling, ...extra].includes(origin) ? origin : null;
},
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Arcrun-API-Key'],
credentials: true,
}));
// 掛載所有路由器
app.route('/', docsRouter);
app.route('/', healthRouter);
app.route('/', executeRouter);
app.route('/', cypherRouter);
app.route('/', validateRouter);
app.route('/', webhooksRouter);
app.route('/', webhooksNamedRouter); // 必須在 webhooksCrudRouter 前(避免 /webhooks/:token 攔截 /webhooks/named
app.route('/', webhooksCrudRouter);
app.route('/', webhooksListRouter);
app.route('/', recipesRouter);
app.route('/', credentialsRouter);
app.route('/', authRouter);
app.route('/', resumeRouter);
app.route('/', executionsRouter); // LI SDD M2.1: /executions/* + /workflows/:name/executions
app.route('/', initSeedRouter); // 薄殼原則:seed recipe 是 API 行為(rule 07,壓測 §4.1
app.route('/', kbdbProxyRouter); // kbdb-base 9.5KBDB 資料層 proxy(讓 CLI 透過 cypher 達 KBDB,純轉發)
app.route('/', consoleAuthRouter); // Arcrun#3 發現②:console 專用簡單 email+password 登入(單一管理員帳密,非多租戶)
app.route('/', consoleDashboardRouter); // T-cockpit ②:駕駛艙 dashboard(聚合 KBDB dash_* entries,無需登入唯讀)
app.route('/', portalRouter); // portal-auth P2#24/#25):RAG Portal 多人授權——用戶模型+認證 API
app.route('/', portalDataRouter); // portal-auth P3/portal/data/* server-side enforceowner_idlibrary 注入,安全核心)
// Worker 導出(fetch + scheduled
// scheduled handler 對應 wrangler.toml [triggers].crons,每分鐘 tick
// 邏輯在 src/scheduled.ts。對應 SDD: arcrun.md 三-A P1 #3。
export default {
fetch: app.fetch,
scheduled: handleScheduled,
} satisfies ExportedHandler<Bindings>;