fix(self-hosted): 身份改明碼 namespace(.env)+ path-based webhook trigger
壓測 §7.2:seed 通了但 creds push/push/runtime 全卡「缺少 api_key」——
self-hosted init 從不發 api_key,但三條路徑都建在多租戶 {api_key}:cred 模型上。
richblack 拍板:self-hosted 不需祕密 api_key,只需 namespace(分區標籤):
- config:ENV_MAP 加 NAMESPACE/ENCRYPTION_KEY + .env 自動載入(無 dotenv 依賴)
- namespace 明碼用戶自填(.env NAMESPACE=leo),沿用 api_key 路徑 → 零分叉
- encryption_key 用戶 .env 自填(工具不生成不 hash),須與 worker secret 一致
- creds/push/init:缺值改引導設 .env,不再叫去 register
- runtime:cypher 加 POST /webhooks/named/:ns/:name/trigger(namespace 走 path,
公開表單免 header);與 header 路徑共用 triggerNamed,不分叉
- push:self-hosted 顯示 path-based 公開 webhook URL
誠實限制:namespace 明碼非密碼;防外部呼叫靠 webhook 保護(mindset §6)。
CLI 1.3.0 → 1.3.1。SDD: self-hosted-init.md §7.7。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,16 +153,27 @@ acr init --self-hosted
|
|||||||
|
|
||||||
你不需要懂 git、不需要懂 tinygo、不需要手動建任何東西——預編譯好的零件(`.wasm`)直接從 GitHub 下載,用**你自己的** CF token 部署到**你的**帳號。
|
你不需要懂 git、不需要懂 tinygo、不需要手動建任何東西——預編譯好的零件(`.wasm`)直接從 GitHub 下載,用**你自己的** CF token 部署到**你的**帳號。
|
||||||
|
|
||||||
**最後一步(手動,CLI 會印提示)**:設定加密金鑰 secret。這一步刻意不自動化(密鑰不進工具流程):
|
**最後一步:身份設定(你自己持有,工具不碰)。** self-hosted 是單租戶——你不需要平台發的 API Key,只需要兩個你自己填的值。在專案建一個 `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env(已被 gitignore;CLI 會自動讀)
|
||||||
|
NAMESPACE=leo # 你的資料分區標籤(明碼即可,這不是密碼)
|
||||||
|
ENCRYPTION_KEY=<64+ hex> # credential 加密金鑰,你自己保管(忘了 = 解不開已上傳的 credential)
|
||||||
|
# 生成 key:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
> `NAMESPACE` 只是「你的資料放哪個分區」的標籤,不是密碼——要防別人呼叫你的 webhook,請對 webhook 加保護(見下)。
|
||||||
|
|
||||||
|
把**同一把** `ENCRYPTION_KEY` 也設進你的 worker(runtime 解密要用,CLI 會印確切指令):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# CLI 會印出確切指令,照貼即可。三個 Worker 共用同一把 key:
|
|
||||||
wrangler secret put ENCRYPTION_KEY --name arcrun-cypher-executor
|
wrangler secret put ENCRYPTION_KEY --name arcrun-cypher-executor
|
||||||
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-static-key
|
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-static-key
|
||||||
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-service-account
|
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-service-account
|
||||||
# 生成一把 key:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 不想自己 put?跑 `acr init` 時明示同意,AI 可代你設——但預設由你自己 put(金鑰是你持有的)。
|
||||||
|
|
||||||
完成。之後有新版零件,跑 `acr update` 一樣自動拉新、重部署。
|
完成。之後有新版零件,跑 `acr update` 一樣自動拉新、重部署。
|
||||||
|
|
||||||
> 想先不碰 Cloudflare、純在本機感受語法?`acr init --local` 然後直接跳到下面「寫一個工作流」。
|
> 想先不碰 Cloudflare、純在本機感受語法?`acr init --local` 然後直接跳到下面「寫一個工作流」。
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
> 由 `scripts/local-deploy.sh` 在 deploy 時自動維護(version bump + 此檔記錄)。
|
> 由 `scripts/local-deploy.sh` 在 deploy 時自動維護(version bump + 此檔記錄)。
|
||||||
> 也可手動編輯補充細節。最新在最上。
|
> 也可手動編輯補充細節。最新在最上。
|
||||||
|
|
||||||
|
## 1.3.1 — 2026-06-06
|
||||||
|
- self-hosted 身份改明碼 namespace(.env 的 NAMESPACE,工具不發 api_key)+ .env 自動載入
|
||||||
|
- creds push / push 缺值時引導設 .env NAMESPACE / ENCRYPTION_KEY(不再叫去 register)
|
||||||
|
- push 對 self-hosted 顯示 path-based 公開 webhook URL(免 header,公開表單可打)
|
||||||
|
- cypher 加 POST /webhooks/named/:ns/:name/trigger(namespace 走 path)
|
||||||
|
- 修壓測 §7.2:self-hosted init 不發 api_key 導致 creds push/push/runtime 全卡
|
||||||
|
|
||||||
## 1.3.0 — 2026-06-06
|
## 1.3.0 — 2026-06-06
|
||||||
- MCP 搬進 arcrun/mcp/ + acr mcp-setup(依 config mcp_url 寫專案 .mcp.json,接案切資料夾自動切 MCP)
|
- MCP 搬進 arcrun/mcp/ + acr mcp-setup(依 config mcp_url 寫專案 .mcp.json,接案切資料夾自動切 MCP)
|
||||||
- config 加 mcp_url 三層解析(env ARCRUN_MCP_URL > 專案 > 全域)+ getMcpUrl()
|
- config 加 mcp_url 三層解析(env ARCRUN_MCP_URL > 專案 > 全域)+ getMcpUrl()
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "arcrun",
|
"name": "arcrun",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "arcrun",
|
"name": "arcrun",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "arcrun",
|
"name": "arcrun",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"description": "AI Workflow CLI for arcrun — self-host WASM-based AI workflows on your own Cloudflare",
|
"description": "AI Workflow CLI for arcrun — self-host WASM-based AI workflows on your own Cloudflare",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acr": "dist/index.js"
|
"acr": "dist/index.js"
|
||||||
|
|||||||
@@ -48,7 +48,15 @@ export async function cmdCredsPush(filePath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!config.api_key) {
|
if (!config.api_key) {
|
||||||
|
// self-hosted 用「資料分區標籤」(明碼,用戶在 .env 設 NAMESPACE)當 KV 前綴,非平台發的 api_key。
|
||||||
|
if (config.mode === 'self-hosted') {
|
||||||
|
console.error(chalk.red('缺少 NAMESPACE(你的資料分區標籤)。'));
|
||||||
|
console.log(chalk.gray('在專案 .env 設一行(明碼即可,這是分區標籤不是密碼):'));
|
||||||
|
console.log(chalk.cyan(' NAMESPACE=leo'));
|
||||||
|
console.log(chalk.gray('(要防外部呼叫請對 webhook 加保護;見 README「讓 AI 連到對的 arcrun」段)'));
|
||||||
|
} else {
|
||||||
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
|
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
|
||||||
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +76,18 @@ export async function cmdCredsPush(filePath: string): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加密金鑰:優先從 config 讀(acr init 時自動寫入),其次從環境變數
|
// 加密金鑰:優先從 config 讀(含 .env 的 ENCRYPTION_KEY / ARCRUN_ENCRYPTION_KEY,見 config.ts loadDotEnvOnce),
|
||||||
|
// 其次環境變數。self-hosted:你自己保管這把(工具不生成、不外傳),須與 worker 的 ENCRYPTION_KEY secret 一致。
|
||||||
const encryptionKey = config.encryption_key ?? process.env.ARCRUN_ENCRYPTION_KEY ?? '';
|
const encryptionKey = config.encryption_key ?? process.env.ARCRUN_ENCRYPTION_KEY ?? '';
|
||||||
if (!encryptionKey || encryptionKey.length < 64) {
|
if (!encryptionKey || encryptionKey.length < 64) {
|
||||||
console.error(chalk.red(
|
if (config.mode === 'self-hosted') {
|
||||||
'缺少 encryption_key。請重新執行 acr init 取得設定。',
|
console.error(chalk.red('缺少 encryption_key(或長度不足,需 ≥64 hex chars = 256-bit)。'));
|
||||||
));
|
console.log(chalk.gray('在專案 .env 設(你自己保管,忘了就解不開已上傳的 credential):'));
|
||||||
|
console.log(chalk.cyan(' ENCRYPTION_KEY=<64+ hex> # 產生:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'));
|
||||||
|
console.log(chalk.gray('同一把也要設進 worker:wrangler secret put ENCRYPTION_KEY(見 acr init 提示)'));
|
||||||
|
} else {
|
||||||
|
console.error(chalk.red('缺少 encryption_key。請重新執行 acr init 取得設定。'));
|
||||||
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,13 +245,21 @@ async function initSelfHosted(
|
|||||||
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
||||||
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
||||||
|
|
||||||
// 手動 secret 提示(secret 不進自動化,rule 05)
|
// 下一步:身份設定(self-hosted 單租戶——namespace 明碼用戶自填、encryption_key 用戶自保管)。
|
||||||
console.log(chalk.bold('\n 下一步(手動設定 runtime secret):'));
|
// 工具不生成、不 hash、不外傳任何 key(守 rule 05 精神:secret 不進自動化,由用戶持有)。
|
||||||
|
console.log(chalk.bold('\n 下一步 ①:在這個專案建 .env(你自己填,工具不碰):'));
|
||||||
|
console.log(chalk.cyan(' NAMESPACE=leo # 你的資料分區標籤(明碼即可,不是密碼)'));
|
||||||
|
console.log(chalk.cyan(' ENCRYPTION_KEY=<64+ hex> # credential 加密金鑰,你自己保管'));
|
||||||
|
console.log(chalk.gray(' 生成 key:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'));
|
||||||
|
console.log(chalk.gray(' (NAMESPACE 是分區標籤非密碼;要防外部呼叫請對 webhook 加保護。'));
|
||||||
|
console.log(chalk.gray(' ENCRYPTION_KEY 忘了 = 解不開已上傳的 credential。.env 已被 gitignore。)'));
|
||||||
|
|
||||||
|
console.log(chalk.bold('\n 下一步 ②:把同一把 ENCRYPTION_KEY 設進你的 worker(runtime 解密要用):'));
|
||||||
for (const w of SECRET_TARGET_WORKERS) {
|
for (const w of SECRET_TARGET_WORKERS) {
|
||||||
console.log(chalk.cyan(` wrangler secret put ENCRYPTION_KEY --name ${w}`));
|
console.log(chalk.cyan(` wrangler secret put ENCRYPTION_KEY --name ${w}`));
|
||||||
}
|
}
|
||||||
console.log(chalk.gray(' 三個 Worker 共用同一把 ENCRYPTION_KEY(256-bit hex)。'));
|
console.log(chalk.gray(` ${SECRET_TARGET_WORKERS.length} 個 Worker 共用同一把(與 .env 的 ENCRYPTION_KEY 一致)。`));
|
||||||
console.log(chalk.gray(' 生成:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"\n'));
|
console.log(chalk.gray(' 不想自己跑?跑 acr init 時授權(明示同意)我可代設——但預設由你自己 put(你持有 key)。\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ export async function cmdPush(filePath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!config.api_key) {
|
if (!config.api_key) {
|
||||||
|
if (config.mode === 'self-hosted') {
|
||||||
|
console.error(chalk.red('缺少 NAMESPACE(你的資料分區標籤)。'));
|
||||||
|
console.log(chalk.gray('在專案 .env 設一行(明碼即可):'));
|
||||||
|
console.log(chalk.cyan(' NAMESPACE=leo'));
|
||||||
|
} else {
|
||||||
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
|
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
|
||||||
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,17 +132,27 @@ export async function cmdPush(filePath: string): Promise<void> {
|
|||||||
const data = await res.json() as { name: string; webhook_url: string; created_at: string };
|
const data = await res.json() as { name: string; webhook_url: string; created_at: string };
|
||||||
deploySpinner.succeed(chalk.green(`✓ "${workflow.name}" 已部署`));
|
deploySpinner.succeed(chalk.green(`✓ "${workflow.name}" 已部署`));
|
||||||
|
|
||||||
|
// self-hosted:namespace 明碼 → 給「namespace 進 path」的公開 URL(公開表單可直接打,免 header)。
|
||||||
|
// standard:仍走 header(平台多租戶,api_key 是密碼不可進 path)。
|
||||||
|
if (config.mode === 'self-hosted') {
|
||||||
|
const pathUrl = `${executorUrl}/webhooks/named/${config.api_key}/${workflow.name}/trigger`;
|
||||||
|
console.log(chalk.bold(`\n Webhook URL(公開可打,免 header):${chalk.cyan(pathUrl)}`));
|
||||||
|
console.log(chalk.gray(' namespace 在 path 是明碼分區標籤(非密碼);要防外部濫用請對 webhook 加保護。'));
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.gray(' 公開表單 / curl 觸發:'));
|
||||||
|
console.log(` ${chalk.cyan(`curl -X POST ${pathUrl} \\`)}`);
|
||||||
|
console.log(` ${chalk.cyan(` -H 'Content-Type: application/json' -d '{"key": "value"}'`)}`);
|
||||||
|
} else {
|
||||||
console.log(chalk.bold(`\n Webhook URL:${chalk.cyan(data.webhook_url)}`));
|
console.log(chalk.bold(`\n Webhook URL:${chalk.cyan(data.webhook_url)}`));
|
||||||
console.log(chalk.gray(` 需帶 Header:X-Arcrun-API-Key: ${config.api_key.slice(0, 8)}...`));
|
console.log(chalk.gray(` 需帶 Header:X-Arcrun-API-Key: ${config.api_key.slice(0, 8)}...`));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(chalk.gray(' 測試執行:'));
|
|
||||||
console.log(` ${chalk.cyan(`acr run ${workflow.name}`)}`);
|
|
||||||
console.log('');
|
|
||||||
console.log(chalk.gray(' curl 觸發範例:'));
|
console.log(chalk.gray(' curl 觸發範例:'));
|
||||||
console.log(` ${chalk.cyan(`curl -X POST ${data.webhook_url} \\`)}`);
|
console.log(` ${chalk.cyan(`curl -X POST ${data.webhook_url} \\`)}`);
|
||||||
console.log(` ${chalk.cyan(` -H 'X-Arcrun-API-Key: ${config.api_key}' \\`)}`);
|
console.log(` ${chalk.cyan(` -H 'X-Arcrun-API-Key: ${config.api_key}' \\`)}`);
|
||||||
console.log(` ${chalk.cyan(` -H 'Content-Type: application/json' \\`)}`);
|
console.log(` ${chalk.cyan(` -H 'Content-Type: application/json' -d '{"key": "value"}'`)}`);
|
||||||
console.log(` ${chalk.cyan(` -d '{"key": "value"}'`)}`);
|
}
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.gray(' 測試執行:') + ` ${chalk.cyan(`acr run ${workflow.name}`)}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
deploySpinner.fail(chalk.red(`部署失敗:${e instanceof Error ? e.message : e}`));
|
deploySpinner.fail(chalk.red(`部署失敗:${e instanceof Error ? e.message : e}`));
|
||||||
|
|||||||
@@ -46,8 +46,15 @@ export type ConfigSource = 'env' | 'project' | 'global' | 'default';
|
|||||||
/** env 變數 → config 欄位映射(最高層覆蓋)。CF 兩個沿用 wrangler 慣用名,CI 設一次兩邊通用。*/
|
/** env 變數 → config 欄位映射(最高層覆蓋)。CF 兩個沿用 wrangler 慣用名,CI 設一次兩邊通用。*/
|
||||||
const ENV_MAP: Record<string, keyof ArcrunConfig> = {
|
const ENV_MAP: Record<string, keyof ArcrunConfig> = {
|
||||||
ARCRUN_MODE: 'mode',
|
ARCRUN_MODE: 'mode',
|
||||||
|
// NAMESPACE / ARCRUN_NAMESPACE:self-hosted 單租戶的資料分區標籤(明碼,用戶自填)。
|
||||||
|
// 沿用 api_key 欄位 + 路徑(KV key 前綴 {api_key}:cred:{name}),故 self-hosted 無需平台發 api_key。
|
||||||
|
// 這是「分區標籤」非「認證密碼」:你的 cypher 在你自己的 CF,無「別人」會冒用;
|
||||||
|
// 要防外部呼叫請對 webhook 加保護(mindset §6)。SaaS 仍走 register 發的真 api_key(同一條路徑,不分叉)。
|
||||||
|
NAMESPACE: 'api_key',
|
||||||
|
ARCRUN_NAMESPACE: 'api_key',
|
||||||
ARCRUN_API_KEY: 'api_key',
|
ARCRUN_API_KEY: 'api_key',
|
||||||
ARCRUN_ENCRYPTION_KEY: 'encryption_key',
|
ARCRUN_ENCRYPTION_KEY: 'encryption_key',
|
||||||
|
ENCRYPTION_KEY: 'encryption_key',
|
||||||
ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url',
|
ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url',
|
||||||
ARCRUN_MCP_URL: 'mcp_url',
|
ARCRUN_MCP_URL: 'mcp_url',
|
||||||
CLOUDFLARE_ACCOUNT_ID: 'cloudflare_account_id',
|
CLOUDFLARE_ACCOUNT_ID: 'cloudflare_account_id',
|
||||||
@@ -93,8 +100,48 @@ function readProjectConfig(): Partial<ArcrunConfig> | undefined {
|
|||||||
return (yaml.load(readFileSync(path, 'utf8')) as Partial<ArcrunConfig>) ?? undefined;
|
return (yaml.load(readFileSync(path, 'utf8')) as Partial<ArcrunConfig>) ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 載入 .env(就近往上找,同 .arcrun.yaml)到 process.env,讓用戶照 Node/Python 慣例
|
||||||
|
* 在 .env 設 NAMESPACE / ENCRYPTION_KEY 等即生效。不覆蓋「已存在於 shell」的 env(shell > .env)。
|
||||||
|
* 自己解析(不引入 dotenv 依賴)。只認單純 KEY=VALUE,忽略空行/註解/引號。
|
||||||
|
*/
|
||||||
|
let _envFileLoaded = false;
|
||||||
|
function loadDotEnvOnce(): void {
|
||||||
|
if (_envFileLoaded) return;
|
||||||
|
_envFileLoaded = true;
|
||||||
|
// 從 cwd 就近往上找 .env(停在含 .arcrun.yaml 的專案根或檔案系統根)
|
||||||
|
let dir = process.cwd();
|
||||||
|
const root = parsePath(dir).root;
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
const candidate = join(dir, '.env');
|
||||||
|
if (existsSync(candidate)) {
|
||||||
|
try {
|
||||||
|
for (const rawLine of readFileSync(candidate, 'utf8').split('\n')) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line || line.startsWith('#')) continue;
|
||||||
|
const eq = line.indexOf('=');
|
||||||
|
if (eq < 1) continue;
|
||||||
|
const k = line.slice(0, eq).trim();
|
||||||
|
let v = line.slice(eq + 1).trim();
|
||||||
|
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
||||||
|
v = v.slice(1, -1);
|
||||||
|
}
|
||||||
|
// shell 已設的優先(不覆蓋),符合「env > .env」直覺
|
||||||
|
if (process.env[k] === undefined) process.env[k] = v;
|
||||||
|
}
|
||||||
|
} catch { /* .env 讀不到不致命 */ }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (dir === root) break;
|
||||||
|
const parent = dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 蒐集 env 覆蓋(只取有設值的 env,欄位級)。*/
|
/** 蒐集 env 覆蓋(只取有設值的 env,欄位級)。*/
|
||||||
function readEnvOverrides(): Partial<ArcrunConfig> {
|
function readEnvOverrides(): Partial<ArcrunConfig> {
|
||||||
|
loadDotEnvOnce();
|
||||||
const out: Partial<ArcrunConfig> = {};
|
const out: Partial<ArcrunConfig> = {};
|
||||||
for (const [envName, field] of Object.entries(ENV_MAP)) {
|
for (const [envName, field] of Object.entries(ENV_MAP)) {
|
||||||
const v = process.env[envName];
|
const v = process.env[envName];
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
import type { Context } from 'hono';
|
||||||
import type { Bindings } from '../types';
|
import type { Bindings } from '../types';
|
||||||
import { executeWebhookGraph } from '../actions/webhook-handlers';
|
import { executeWebhookGraph } from '../actions/webhook-handlers';
|
||||||
import { writeExecutionVerdict } from '../actions/execution-logger';
|
import { writeExecutionVerdict } from '../actions/execution-logger';
|
||||||
@@ -130,14 +131,29 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => {
|
|||||||
}, 201);
|
}, 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /webhooks/named/:name/trigger — 觸發執行
|
// POST /webhooks/named/:name/trigger — 觸發執行(api_key 走 header;標準/向後相容)
|
||||||
webhooksNamedRouter.post('/webhooks/named/:name/trigger', async (c) => {
|
webhooksNamedRouter.post('/webhooks/named/:name/trigger', async (c) => {
|
||||||
const apiKey = c.req.header('X-Arcrun-API-Key');
|
const apiKey = c.req.header('X-Arcrun-API-Key');
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
|
return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
|
||||||
}
|
}
|
||||||
|
return triggerNamed(c, apiKey, c.req.param('name'));
|
||||||
|
});
|
||||||
|
|
||||||
const name = c.req.param('name');
|
// POST /webhooks/named/:ns/:name/trigger — namespace 走 URL path(給公開表單用)。
|
||||||
|
// self-hosted namespace 是明碼分區標籤(非密碼),故可放 path 讓無法帶 header 的公開呼叫者觸發。
|
||||||
|
// 要防外部濫用 → 對 webhook 加保護(mindset §6);arcrun 不做授權判斷(mindset §3)。
|
||||||
|
// SDD: sdk-and-website/self-hosted-init.md(壓測 §7.2 第3點 runtime 觸發)
|
||||||
|
webhooksNamedRouter.post('/webhooks/named/:ns/:name/trigger', async (c) => {
|
||||||
|
return triggerNamed(c, c.req.param('ns'), c.req.param('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 共用觸發邏輯(header 路徑與 path 路徑都用,避免分叉)
|
||||||
|
async function triggerNamed(
|
||||||
|
c: Context<{ Bindings: Bindings }>,
|
||||||
|
apiKey: string,
|
||||||
|
name: string,
|
||||||
|
) {
|
||||||
const raw = await c.env.WEBHOOKS.get(kvKey(apiKey, name), 'text');
|
const raw = await c.env.WEBHOOKS.get(kvKey(apiKey, name), 'text');
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return c.json({ error: `找不到 workflow "${name}",請先執行 acr push` }, 404);
|
return c.json({ error: `找不到 workflow "${name}",請先執行 acr push` }, 404);
|
||||||
@@ -192,7 +208,7 @@ webhooksNamedRouter.post('/webhooks/named/:name/trigger', async (c) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return c.json(result, result.success ? 200 : 500);
|
return c.json(result, result.success ? 200 : 500);
|
||||||
});
|
}
|
||||||
|
|
||||||
// GET /webhooks/named — 列出當前 api_key 下所有 workflow
|
// GET /webhooks/named — 列出當前 api_key 下所有 workflow
|
||||||
webhooksNamedRouter.get('/webhooks/named', async (c) => {
|
webhooksNamedRouter.get('/webhooks/named', async (c) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user