07cc7f51b5
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>
108 lines
6.1 KiB
TypeScript
108 lines
6.1 KiB
TypeScript
// 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-split:console/portal UI 搬到 Cloudflare Pages 後,前端與本 API
|
||
// **不再同源**,故 UI 站的 origin 必須進白名單,否則所有 fetch 會被瀏覽器擋。
|
||
// 允許來源=上面兩個 landing + UI_ORIGINS(wrangler.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) => {
|
||
// ⚠️ 非瀏覽器請求(CLI/curl/MCP)沒有 Origin 標頭 → origin 是空字串/undefined。
|
||
// 此時必須原樣放行,不能回 null——回 null 會讓 Hono cors 中介層在後續處理拋錯,
|
||
// 表現為所有 CLI 部署一律 500(2026-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.5:KBDB 資料層 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 enforce(owner_id+library 注入,安全核心)
|
||
|
||
// 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>;
|