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:
@@ -46,8 +46,15 @@ export type ConfigSource = 'env' | 'project' | 'global' | 'default';
|
||||
/** env 變數 → config 欄位映射(最高層覆蓋)。CF 兩個沿用 wrangler 慣用名,CI 設一次兩邊通用。*/
|
||||
const ENV_MAP: Record<string, keyof ArcrunConfig> = {
|
||||
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_ENCRYPTION_KEY: 'encryption_key',
|
||||
ENCRYPTION_KEY: 'encryption_key',
|
||||
ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url',
|
||||
ARCRUN_MCP_URL: 'mcp_url',
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 載入 .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,欄位級)。*/
|
||||
function readEnvOverrides(): Partial<ArcrunConfig> {
|
||||
loadDotEnvOnce();
|
||||
const out: Partial<ArcrunConfig> = {};
|
||||
for (const [envName, field] of Object.entries(ENV_MAP)) {
|
||||
const v = process.env[envName];
|
||||
|
||||
Reference in New Issue
Block a user