ai-fly 0.4.0-alpha.2 → 0.4.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-fly.js +8 -7
- package/dist/app/main.js +205 -99
- package/dist/{common-Cr7D8JyX.mjs → common-BerjPDN0.mjs} +1 -1
- package/dist/{common-BwKEJKOZ.mjs → common-DGktbuQT.mjs} +17 -6
- package/dist/{daemon-C4kj0IwL.mjs → daemon-z7hZDf5X.mjs} +3 -3
- package/dist/{forget-oMwM-SsU.mjs → forget-zXXMWRPE.mjs} +2 -2
- package/dist/{frames-BDqvhB2Y.mjs → frames-BDz8buix.mjs} +1 -0
- package/dist/{group-CjwGDch3.mjs → group-DxTSU3w2.mjs} +1 -1
- package/dist/hook-CvAMGts4.mjs +228 -0
- package/dist/hooks-Da7bL31j.mjs +142 -0
- package/dist/{import-Zy3LL_PR.mjs → import-DVD6ODYu.mjs} +4 -4
- package/dist/{join-CdKKj7QS.mjs → join-BjpRt0Tj.mjs} +3 -3
- package/dist/{join-BJMcNGV-.mjs → join-BvMm1-Qz.mjs} +3 -3
- package/dist/{key-TP7lZeqW.mjs → key-BvFO4hCw.mjs} +1 -1
- package/dist/{key-BcNxtRJx.mjs → key-DHyfw_xd.mjs} +3 -3
- package/dist/{local-test-DbpiPEsD.mjs → local-test-CPOZODTW.mjs} +1 -1
- package/dist/{models-dev-7CclWuHF.mjs → models-dev-DhEP4LJH.mjs} +7 -3
- package/dist/{mux-BEXBL7rE.mjs → mux-BQ1w82tz.mjs} +2 -2
- package/dist/{ports-Csv91JOD.mjs → ports-Bs8NpX1c.mjs} +2 -2
- package/dist/{presets-DRO5N4Vm.mjs → presets-B339R3JU.mjs} +2 -2
- package/dist/{revoke-CXTcFgoT.mjs → revoke-CA4VYdTU.mjs} +2 -2
- package/dist/{rewrite-BL3jNnFZ.mjs → rewrite-zdPePt_u.mjs} +41 -92
- package/dist/{route-test-DqKRThLp.mjs → route-test-D8se2bKr.mjs} +4 -4
- package/dist/{rpc-contract-CdiRZhUn.mjs → rpc-contract-eOK7IGzh.mjs} +21 -6
- package/dist/{run-W7DBz_bR.mjs → run-Ckodfe9X.mjs} +3 -3
- package/dist/{runtime-CGF9Sndm.mjs → runtime-CJWL72hJ.mjs} +3 -3
- package/dist/{secret-DKN75Vry.mjs → secret-BTq8J0Oj.mjs} +2 -2
- package/dist/{secrets-DFdwYE_V.mjs → secrets-BkwvIX0M.mjs} +2 -2
- package/dist/secrets-DtqxPHvY.mjs +2 -0
- package/dist/{serve-FjBsmK5n.mjs → serve-CEmNAhX5.mjs} +2 -2
- package/dist/{serve-GRJthOwO.mjs → serve-CvGVQszo.mjs} +6 -6
- package/dist/{service-DnQhGK4m.mjs → service-CBURGJU1.mjs} +19 -6
- package/dist/{settings-Dho5hk5Z.mjs → settings-BwDXTwUJ.mjs} +1 -1
- package/dist/{settings-BAa8I7Ww.mjs → settings-D1Ukj8bF.mjs} +2 -2
- package/dist/{share-DpQPO1eC.mjs → share-CNYqhoFY.mjs} +5 -5
- package/dist/{status-BaRyycxh.mjs → status-DlB1AHty.mjs} +3 -3
- package/dist/{status-Ce-lxy52.mjs → status-bV9YPNmT.mjs} +3 -3
- package/dist/{store-jmpkM28T.mjs → store-DUMRSQVn.mjs} +2 -2
- package/dist/{store-YUMvMCBg.mjs → store-z7qZO_5S.mjs} +9 -2
- package/dist/{test-Dxkkc-KN.mjs → test-DtOSl9gb.mjs} +2 -2
- package/package.json +1 -1
- package/webui/dist/assets/{index-0TnWjNwU.js → index-B52MRIjg.js} +3 -3
- package/webui/dist/index.html +1 -1
- package/dist/secrets-BKKxR3iB.mjs +0 -2
package/dist/ai-fly.js
CHANGED
|
@@ -15,7 +15,7 @@ Daemon:
|
|
|
15
15
|
ai-fly app launch the tray GUI (desktop app)
|
|
16
16
|
|
|
17
17
|
Provider:
|
|
18
|
-
ai-fly service add <name> [--upstream <url> | --preset <id>] [--
|
|
18
|
+
ai-fly service add <name> [--upstream <url> | --preset <id>] [--hooks <script>] [--route ...]...
|
|
19
19
|
[--route-pattern <match>=<template>]... [--secret <name>]
|
|
20
20
|
[--port <n>] [--match <type>:<value>]... [--data <dir>]
|
|
21
21
|
ai-fly service list|get|remove <name> [--data <dir>]
|
|
@@ -26,6 +26,7 @@ Provider:
|
|
|
26
26
|
ai-fly share --group <name> [--ttl <dur>] mint a share link (token + key)
|
|
27
27
|
ai-fly revoke <endpointId> eject a device (fabric-level)
|
|
28
28
|
ai-fly presets [search] [--json] featured + models.dev long tail
|
|
29
|
+
ai-fly hooks list|get|add|remove|run hook scripts (builtin + ~/.aifly/hooks)
|
|
29
30
|
|
|
30
31
|
Consumer:
|
|
31
32
|
ai-fly join <dweb1-token> [--data <dir>] [--proxy …] admit this device (fabric layer)
|
|
@@ -62,7 +63,7 @@ async function dispatch(command, rest) {
|
|
|
62
63
|
const lazy = (load) => {
|
|
63
64
|
return (argv, ctx) => load().then((m) => m.run([...argv], ctx));
|
|
64
65
|
};
|
|
65
|
-
return match(command).with("serve", () => lazy(() => import("./serve-
|
|
66
|
+
return match(command).with("serve", () => lazy(() => import("./serve-CEmNAhX5.mjs"))(rest)).with("app", async () => {
|
|
66
67
|
const { join, dirname } = await import("node:path");
|
|
67
68
|
const { fileURLToPath } = await import("node:url");
|
|
68
69
|
const { spawn } = await import("node:child_process");
|
|
@@ -77,17 +78,17 @@ async function dispatch(command, rest) {
|
|
|
77
78
|
return await new Promise((resolve) => {
|
|
78
79
|
child.once("exit", (c) => resolve(c ?? 0));
|
|
79
80
|
});
|
|
80
|
-
}).with("daemon", () => lazy(() => import("./daemon-
|
|
81
|
-
if (rest[0] === "add") return lazy(() => import("./key-
|
|
82
|
-
return lazy(() => import("./key-
|
|
81
|
+
}).with("daemon", () => lazy(() => import("./daemon-z7hZDf5X.mjs"))(rest)).with("presets", () => lazy(() => import("./presets-B339R3JU.mjs"))(rest)).with("hooks", () => lazy(() => import("./hooks-Da7bL31j.mjs"))(rest)).with("secret", () => lazy(() => import("./secret-BTq8J0Oj.mjs"))(rest)).with("settings", async () => (await import("./settings-BwDXTwUJ.mjs")).runAsSettings(rest, {})).with("relay", async () => (await import("./settings-BwDXTwUJ.mjs")).runAsRelay(rest, {})).with("test", () => lazy(() => import("./test-DtOSl9gb.mjs"))(rest)).with("service", () => lazy(() => import("./service-CBURGJU1.mjs"))(rest)).with("group", () => lazy(() => import("./group-DxTSU3w2.mjs"))(rest)).with("share", () => lazy(() => import("./share-CNYqhoFY.mjs"))(rest)).with("revoke", () => lazy(() => import("./revoke-CA4VYdTU.mjs"))(rest)).with("join", () => lazy(() => import("./join-BvMm1-Qz.mjs"))(rest)).with("import", () => lazy(() => import("./import-DVD6ODYu.mjs"))(rest)).with("run", () => lazy(() => import("./run-Ckodfe9X.mjs"))(rest)).with("ports", () => lazy(() => import("./ports-Bs8NpX1c.mjs"))(rest)).with("forget", () => lazy(() => import("./forget-zXXMWRPE.mjs"))(rest)).with("key", () => {
|
|
82
|
+
if (rest[0] === "add") return lazy(() => import("./key-DHyfw_xd.mjs"))(rest);
|
|
83
|
+
return lazy(() => import("./key-BvFO4hCw.mjs"))(rest);
|
|
83
84
|
}).with("status", async () => {
|
|
84
85
|
const [scope, ...tail] = rest;
|
|
85
86
|
const wantProvider = scope === "provider" || scope !== "consumer" && providerDirExists();
|
|
86
87
|
const wantConsumer = scope === "consumer" || scope !== "provider" && consumersRootExists();
|
|
87
88
|
const argv = scope === "provider" || scope === "consumer" ? tail : rest;
|
|
88
89
|
let code = 0;
|
|
89
|
-
if (wantProvider) code = Math.max(code, await lazy(() => import("./status-
|
|
90
|
-
if (wantConsumer) code = Math.max(code, await lazy(() => import("./status-
|
|
90
|
+
if (wantProvider) code = Math.max(code, await lazy(() => import("./status-bV9YPNmT.mjs"))(argv));
|
|
91
|
+
if (wantConsumer) code = Math.max(code, await lazy(() => import("./status-DlB1AHty.mjs"))(argv));
|
|
91
92
|
if (!wantProvider && !wantConsumer) process.stdout.write("nothing to report: no provider storage and no imported consumers\n");
|
|
92
93
|
return code;
|
|
93
94
|
}).otherwise(() => {
|
package/dist/app/main.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir, platform } from "node:os";
|
|
3
4
|
import path, { dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -59,7 +60,11 @@ const SERVICE_REWRITE_SCHEMA = z.strictObject({
|
|
|
59
60
|
hostHeader: z.string().min(1).max(2048).optional(),
|
|
60
61
|
pathPrefixStrip: z.string().min(1).max(2048).optional(),
|
|
61
62
|
pathPrefixAppend: z.string().min(1).max(2048).optional(),
|
|
62
|
-
headerSet: z.record(z.string().min(1).max(1024), z.string().max(8192)
|
|
63
|
+
headerSet: z.record(z.string().min(1).max(1024), z.union([z.string().max(8192), z.strictObject({
|
|
64
|
+
hook: z.string().min(1).max(128),
|
|
65
|
+
args: z.record(z.string().min(1).max(128), z.string().max(2048)).optional(),
|
|
66
|
+
bearer: z.boolean().optional()
|
|
67
|
+
})])).optional(),
|
|
63
68
|
headerRemove: z.array(z.string().min(1).max(1024)).max(32).optional()
|
|
64
69
|
});
|
|
65
70
|
const ROUTE_FORM_SCHEMA = z.enum([
|
|
@@ -107,6 +112,7 @@ const SERVICE_SCHEMA = z.strictObject({
|
|
|
107
112
|
upstream: z.string().min(1).max(2048),
|
|
108
113
|
rewrite: SERVICE_REWRITE_SCHEMA.optional(),
|
|
109
114
|
routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional(),
|
|
115
|
+
hooks: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/).optional(),
|
|
110
116
|
defaultPort: z.number().int().min(1).max(65535)
|
|
111
117
|
});
|
|
112
118
|
/** services.add 输入(defaultPort 缺省规则的校验在 store,同 CLI)。 */
|
|
@@ -116,7 +122,8 @@ const SERVICE_INPUT_SCHEMA = z.strictObject({
|
|
|
116
122
|
match: z.array(SERVICE_MATCH_SCHEMA$1).min(1).max(64),
|
|
117
123
|
defaultPort: z.number().int().min(1).max(65535).optional(),
|
|
118
124
|
rewrite: SERVICE_REWRITE_SCHEMA.optional(),
|
|
119
|
-
routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional()
|
|
125
|
+
routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional(),
|
|
126
|
+
hooks: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/).optional()
|
|
120
127
|
});
|
|
121
128
|
const GROUP_LIMITS_SCHEMA = z.strictObject({
|
|
122
129
|
maxConcurrency: z.number().int().min(1).optional(),
|
|
@@ -154,7 +161,11 @@ const SERVICE_ENTRY_SCHEMA$1 = z.object({
|
|
|
154
161
|
prefix: z.string().optional(),
|
|
155
162
|
headerSet: z.array(z.strictObject({
|
|
156
163
|
name: z.string(),
|
|
157
|
-
value: z.string()
|
|
164
|
+
value: z.union([z.string(), z.strictObject({
|
|
165
|
+
hook: z.string(),
|
|
166
|
+
args: z.record(z.string(), z.string()).optional(),
|
|
167
|
+
bearer: z.boolean().optional()
|
|
168
|
+
})])
|
|
158
169
|
})).optional()
|
|
159
170
|
}).optional(),
|
|
160
171
|
/** 按标准路由披露(消费侧呈现各标准本地 base 与可用性判定)。 */
|
|
@@ -205,9 +216,14 @@ const PRESET_SCHEMA = z.strictObject({
|
|
|
205
216
|
iconId: z.string().min(1).max(128).optional(),
|
|
206
217
|
/** 惯用环境变量名(仅用于 $env 注入建议与文档;本地运行时模板无此字段)。 */
|
|
207
218
|
keyEnv: z.string().min(1).max(256).optional(),
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
authHeader
|
|
219
|
+
/** 选中的 hooks 脚本名(applyAsService 透传为服务 hooks 字段)。 */
|
|
220
|
+
hooks: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/).optional(),
|
|
221
|
+
/** 预填 authorization 头的钩子调用对象;优先级:显式 secretName > authHeader > keyEnv。 */
|
|
222
|
+
authHeader: z.strictObject({
|
|
223
|
+
hook: z.string().min(1).max(128),
|
|
224
|
+
args: z.record(z.string().min(1).max(128), z.string().max(2048)).optional(),
|
|
225
|
+
bearer: z.boolean().optional()
|
|
226
|
+
}).optional(),
|
|
211
227
|
/** 使用方本地端口建议(避开 <1024 特权段)。 */
|
|
212
228
|
defaultPort: z.number().int().min(1024).max(65535),
|
|
213
229
|
/** 官方域名集(exact/suffix 建议的生成源)。 */
|
|
@@ -838,7 +854,11 @@ const SERVICE_REWRITE_STORE_SCHEMA = z.strictObject({
|
|
|
838
854
|
hostHeader: z.string().min(1).max(2048).optional(),
|
|
839
855
|
pathPrefixStrip: z.string().min(1).max(2048).optional(),
|
|
840
856
|
pathPrefixAppend: z.string().min(1).max(2048).optional(),
|
|
841
|
-
headerSet: z.record(z.string().min(1).max(1024), z.string().max(8192)
|
|
857
|
+
headerSet: z.record(z.string().min(1).max(1024), z.union([z.string().max(8192), z.strictObject({
|
|
858
|
+
hook: z.string().min(1).max(128),
|
|
859
|
+
args: z.record(z.string().min(1).max(128), z.string().max(2048)).optional(),
|
|
860
|
+
bearer: z.boolean().optional()
|
|
861
|
+
})])).optional(),
|
|
842
862
|
headerRemove: z.array(z.string().min(1).max(1024)).max(32).optional()
|
|
843
863
|
});
|
|
844
864
|
/** 路径路由(M3-r7):按声明顺序命中的转发规则——prefix 模式(默认,
|
|
@@ -862,6 +882,8 @@ const SERVICE_STORE_SCHEMA = z.strictObject({
|
|
|
862
882
|
match: z.array(SERVICE_MATCH_STORE_SCHEMA).max(64),
|
|
863
883
|
upstream: z.string().min(1).max(2048),
|
|
864
884
|
rewrite: SERVICE_REWRITE_STORE_SCHEMA.optional(),
|
|
885
|
+
/** 选中的 hooks 脚本名(内建库或 ~/.aifly/hooks/<name>.cjs)。 */
|
|
886
|
+
hooks: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/).optional(),
|
|
865
887
|
routes: z.array(SERVICE_ROUTE_STORE_SCHEMA).max(3).optional(),
|
|
866
888
|
defaultPort: z.number().int().min(1).max(65535)
|
|
867
889
|
});
|
|
@@ -1013,6 +1035,7 @@ var ProviderStore = class ProviderStore {
|
|
|
1013
1035
|
match: input.match.map((m) => ({ ...m })),
|
|
1014
1036
|
upstream: upstreamUrl.href,
|
|
1015
1037
|
rewrite,
|
|
1038
|
+
...input.hooks !== void 0 ? { hooks: input.hooks } : {},
|
|
1016
1039
|
...routes !== void 0 ? { routes } : {},
|
|
1017
1040
|
defaultPort
|
|
1018
1041
|
};
|
|
@@ -1612,6 +1635,7 @@ const SERVICE_ENTRY_SCHEMA = z.strictObject({
|
|
|
1612
1635
|
name: z.string().min(1).max(256),
|
|
1613
1636
|
match: z.array(SERVICE_MATCH_SCHEMA).max(64),
|
|
1614
1637
|
defaultPort: z.number().int().min(1).max(65535),
|
|
1638
|
+
hooks: z.string().optional(),
|
|
1615
1639
|
detail: SERVICE_DETAIL_SCHEMA.optional()
|
|
1616
1640
|
});
|
|
1617
1641
|
const GROUP_ENTRY_SCHEMA = z.strictObject({
|
|
@@ -2416,7 +2440,7 @@ function buildServiceDetail(service) {
|
|
|
2416
2440
|
if (prefix !== void 0) rewrite.prefix = prefix;
|
|
2417
2441
|
if (service.rewrite.headerSet !== void 0) rewrite.headerSet = Object.entries(service.rewrite.headerSet).map(([name, value]) => ({
|
|
2418
2442
|
name,
|
|
2419
|
-
value: isMaskedRef(value) ? "●" : value
|
|
2443
|
+
value: typeof value === "string" ? isMaskedRef(value) ? "●" : value : "●"
|
|
2420
2444
|
}));
|
|
2421
2445
|
}
|
|
2422
2446
|
return {
|
|
@@ -2554,6 +2578,122 @@ var KeySessionIndex = class {
|
|
|
2554
2578
|
}
|
|
2555
2579
|
};
|
|
2556
2580
|
//#endregion
|
|
2581
|
+
//#region src/provider/hook.ts
|
|
2582
|
+
var HookMissingError = class extends Error {
|
|
2583
|
+
constructor() {
|
|
2584
|
+
super("credential source missing");
|
|
2585
|
+
this.name = "HookMissingError";
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
/** 包根(向上找 name=ai-fly 的 package.json;dev 仓库与安装布局皆成立)。 */
|
|
2589
|
+
function packageRoot(start = dirname(fileURLToPath(import.meta.url))) {
|
|
2590
|
+
let dir = resolve(start);
|
|
2591
|
+
for (let hops = 0; hops < 12; hops += 1) {
|
|
2592
|
+
const pkg = join(dir, "package.json");
|
|
2593
|
+
if (existsSync(pkg)) try {
|
|
2594
|
+
const parsed = JSON.parse(readFileSync(pkg, "utf8"));
|
|
2595
|
+
if (parsed !== null && typeof parsed === "object" && parsed.name === "ai-fly") return dir;
|
|
2596
|
+
} catch {}
|
|
2597
|
+
const parent = dirname(dir);
|
|
2598
|
+
if (parent === dir) break;
|
|
2599
|
+
dir = parent;
|
|
2600
|
+
}
|
|
2601
|
+
return process.cwd();
|
|
2602
|
+
}
|
|
2603
|
+
function hookScriptPaths(home = homedir()) {
|
|
2604
|
+
return {
|
|
2605
|
+
userDir: join(home, ".aifly", "hooks"),
|
|
2606
|
+
builtinDir: join(packageRoot(), "hooks")
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
function scriptCandidates(name, paths) {
|
|
2610
|
+
const bases = [join(paths.userDir, name), join(paths.builtinDir, name)];
|
|
2611
|
+
const out = [];
|
|
2612
|
+
for (const b of bases) out.push(`${b}.cjs`, b.endsWith(".cjs") || b.endsWith(".js") ? b : `${b}.js`);
|
|
2613
|
+
return out;
|
|
2614
|
+
}
|
|
2615
|
+
/** 加载 hooks 脚本模块(用户库优先于内建库;缺席返回 undefined)。 */
|
|
2616
|
+
function loadHookScript(name, home = homedir()) {
|
|
2617
|
+
if (name === "" || name.includes("/") || name.includes("..")) return void 0;
|
|
2618
|
+
const req = createRequire(import.meta.url);
|
|
2619
|
+
for (const candidate of scriptCandidates(name, hookScriptPaths(home))) {
|
|
2620
|
+
if (!existsSync(candidate)) continue;
|
|
2621
|
+
try {
|
|
2622
|
+
const mod = req(candidate);
|
|
2623
|
+
if (mod !== null && typeof mod === "object") return mod;
|
|
2624
|
+
} catch {}
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
function isAsyncIterable(v) {
|
|
2628
|
+
return v !== null && typeof v === "object" && Symbol.asyncIterator in v;
|
|
2629
|
+
}
|
|
2630
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
2631
|
+
/** 建立订阅并等待首个 yield:返回首值(后续值由后台循环更新 latest)。 */
|
|
2632
|
+
function startSubscription(key, stream, sub) {
|
|
2633
|
+
sub.iterator = stream[Symbol.asyncIterator]();
|
|
2634
|
+
return new Promise((resolveFirst) => {
|
|
2635
|
+
let firstSettled = false;
|
|
2636
|
+
(async () => {
|
|
2637
|
+
try {
|
|
2638
|
+
const it = sub.iterator;
|
|
2639
|
+
for (;;) {
|
|
2640
|
+
const r = await it.next();
|
|
2641
|
+
if (r.done === true) break;
|
|
2642
|
+
if (typeof r.value === "string" && r.value !== "") {
|
|
2643
|
+
sub.latest = r.value;
|
|
2644
|
+
if (firstSettled === false) {
|
|
2645
|
+
firstSettled = true;
|
|
2646
|
+
resolveFirst(r.value);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
} catch {}
|
|
2651
|
+
if (firstSettled === false) {
|
|
2652
|
+
firstSettled = true;
|
|
2653
|
+
resolveFirst(void 0);
|
|
2654
|
+
}
|
|
2655
|
+
})();
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* 解析一次钩子取值(headerSet 的 {hook,...} 协议消费点)。
|
|
2660
|
+
* 订阅键 = script|fn —— 同键首个请求建立订阅,后续读 latest。
|
|
2661
|
+
* 任何未命中(脚本/函数缺席、调用抛错、非字符串、空串、订阅未出首值)
|
|
2662
|
+
* 抛 HookMissingError(零上游请求;信息不含脚本路径与值)。
|
|
2663
|
+
*/
|
|
2664
|
+
async function resolveHookValue(fnName, opts) {
|
|
2665
|
+
const home = opts.home ?? homedir();
|
|
2666
|
+
const raw = (opts.loader ?? loadHookScript)(opts.script, home)?.[fnName];
|
|
2667
|
+
if (typeof raw !== "function") throw new HookMissingError();
|
|
2668
|
+
let result;
|
|
2669
|
+
try {
|
|
2670
|
+
result = raw({
|
|
2671
|
+
homedir: home,
|
|
2672
|
+
args: opts.args ?? {},
|
|
2673
|
+
secrets: (n) => opts.secrets?.(n),
|
|
2674
|
+
env: (n) => opts.env?.(n) ?? process.env[n]
|
|
2675
|
+
});
|
|
2676
|
+
} catch {
|
|
2677
|
+
throw new HookMissingError();
|
|
2678
|
+
}
|
|
2679
|
+
const key = `${opts.script}|${fnName}`;
|
|
2680
|
+
if (isAsyncIterable(result)) {
|
|
2681
|
+
let sub = subscriptions.get(key);
|
|
2682
|
+
if (sub === void 0) {
|
|
2683
|
+
sub = {};
|
|
2684
|
+
subscriptions.set(key, sub);
|
|
2685
|
+
const first = await startSubscription(key, result, sub);
|
|
2686
|
+
if (first === void 0) throw new HookMissingError();
|
|
2687
|
+
return first;
|
|
2688
|
+
}
|
|
2689
|
+
if (sub.latest === void 0) throw new HookMissingError();
|
|
2690
|
+
return sub.latest;
|
|
2691
|
+
}
|
|
2692
|
+
const awaited = await result;
|
|
2693
|
+
if (typeof awaited !== "string" || awaited === "") throw new HookMissingError();
|
|
2694
|
+
return awaited;
|
|
2695
|
+
}
|
|
2696
|
+
//#endregion
|
|
2557
2697
|
//#region src/provider/rewrite.ts
|
|
2558
2698
|
/** 拼接/断言失败(protocol_error 语义)。 */
|
|
2559
2699
|
var RewriteError = class extends Error {
|
|
@@ -2584,100 +2724,45 @@ var SecretMissingError = class extends Error {
|
|
|
2584
2724
|
}
|
|
2585
2725
|
};
|
|
2586
2726
|
/**
|
|
2587
|
-
*
|
|
2588
|
-
*
|
|
2589
|
-
*
|
|
2590
|
-
*
|
|
2727
|
+
* 头值解析(两协议,Owner 裁决 2026-09-12 最终形态):
|
|
2728
|
+
* - string 字面量:原样(空串 = 省略该头)
|
|
2729
|
+
* - 对象 = 钩子调用:{ hook: <函数名>, args?, bearer? }——脚本取
|
|
2730
|
+
* ctx.script(service.hooks),缺席时按 args 形态推导内建
|
|
2731
|
+
* (var→env / name→secret / path→file);经 resolveHookValue 三态取值
|
|
2732
|
+
* (string/Promise 每请求拉取;AsyncIterable 订阅 latest);bearer 拼
|
|
2733
|
+
* 前缀。未命中抛 SecretMissingError(零上游请求;不泄脚本路径与值)。
|
|
2591
2734
|
*/
|
|
2592
|
-
function
|
|
2593
|
-
|
|
2594
|
-
const
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
}
|
|
2602
|
-
if (spec === "" || !spec.startsWith(".")) return null;
|
|
2603
|
-
return {
|
|
2604
|
-
path: body.slice(0, hash),
|
|
2605
|
-
jsonPath: spec,
|
|
2606
|
-
bearer
|
|
2607
|
-
};
|
|
2608
|
-
}
|
|
2609
|
-
/** jq 风格点径求值(`.a.b` / `.a[0].b`;起点须以 `.` 开头)。未命中返回 undefined。 */
|
|
2610
|
-
function evalJsonPath(root, jsonPath) {
|
|
2611
|
-
if (!jsonPath.startsWith(".")) return void 0;
|
|
2612
|
-
let cur = root;
|
|
2613
|
-
const segments = jsonPath.slice(1).split(/(?=\[)|\./).filter((s) => s !== "");
|
|
2614
|
-
for (const seg of segments) {
|
|
2615
|
-
if (cur === null || cur === void 0) return void 0;
|
|
2616
|
-
const arr = /^\[(\d+)\]$/.exec(seg);
|
|
2617
|
-
if (arr !== null) {
|
|
2618
|
-
if (!Array.isArray(cur)) return void 0;
|
|
2619
|
-
cur = cur[Number(arr[1])];
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
if (typeof cur !== "object") return void 0;
|
|
2623
|
-
cur = cur[seg];
|
|
2735
|
+
async function resolveHeaderEntry(entry, ctx) {
|
|
2736
|
+
if (typeof entry === "string") return entry === "" ? void 0 : entry;
|
|
2737
|
+
const args = entry.args ?? {};
|
|
2738
|
+
let script = ctx.script;
|
|
2739
|
+
if (script === void 0 || script === "") {
|
|
2740
|
+
if (args.var !== void 0) script = "env";
|
|
2741
|
+
else if (args.name !== void 0) script = "secret";
|
|
2742
|
+
else if (args.path !== void 0) script = "file";
|
|
2743
|
+
else throw new SecretMissingError();
|
|
2624
2744
|
}
|
|
2625
|
-
return cur;
|
|
2626
|
-
}
|
|
2627
|
-
/** 默认文件凭据源:读文件 + JSON 解析 + 点径求值(仅字符串值;`~` 已在上层展开)。 */
|
|
2628
|
-
const defaultFileCredentialSource = (path, jsonPath) => {
|
|
2629
2745
|
let raw;
|
|
2630
2746
|
try {
|
|
2631
|
-
raw =
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2747
|
+
raw = await resolveHookValue(entry.hook, {
|
|
2748
|
+
script,
|
|
2749
|
+
...ctx.home !== void 0 ? { home: ctx.home } : {},
|
|
2750
|
+
...Object.keys(args).length > 0 ? { args } : {},
|
|
2751
|
+
...ctx.secrets !== void 0 ? { secrets: ctx.secrets } : {},
|
|
2752
|
+
...ctx.env !== void 0 ? { env: (n) => ctx.env?.[n] } : {},
|
|
2753
|
+
...ctx.loader !== void 0 ? { loader: ctx.loader } : {}
|
|
2754
|
+
});
|
|
2638
2755
|
} catch {
|
|
2639
|
-
|
|
2756
|
+
throw new SecretMissingError();
|
|
2640
2757
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
/**
|
|
2645
|
-
* 头值解析:literal 原样;`$secret:<name>` 优先于 `$env:`/`$file:` 判定——命中
|
|
2646
|
-
* 密钥库返回完整值,未命中(含空名/空值/未注入密钥源)抛 SecretMissingError
|
|
2647
|
-
* (不回退空值、不省略该头);`$file:<path>#<json-path>[?bearer]` 每请求读盘
|
|
2648
|
-
* (文件/键/空值未命中同抛 SecretMissingError;`?bearer` 拼前缀);
|
|
2649
|
-
* `$env:<VAR>` 语义保持——空串/未设置返回 undefined(= 省略该头)。
|
|
2650
|
-
* 各源可并存于不同头(逐头独立解析)。
|
|
2651
|
-
*/
|
|
2652
|
-
function resolveHeaderValue(value, env, secrets, files = defaultFileCredentialSource) {
|
|
2653
|
-
if (value.startsWith("$secret:")) {
|
|
2654
|
-
const name = value.slice(8);
|
|
2655
|
-
const resolved = secrets?.(name);
|
|
2656
|
-
if (resolved === void 0 || resolved === "") throw new SecretMissingError();
|
|
2657
|
-
return resolved;
|
|
2658
|
-
}
|
|
2659
|
-
if (value.startsWith("$file:")) {
|
|
2660
|
-
const ref = parseFileRef(value);
|
|
2661
|
-
if (ref === null) throw new SecretMissingError();
|
|
2662
|
-
const resolved = files(ref.path.startsWith("~/") ? join(homedir(), ref.path.slice(1)) : ref.path, ref.jsonPath);
|
|
2663
|
-
if (resolved === void 0 || resolved === "") throw new SecretMissingError();
|
|
2664
|
-
return ref.bearer ? `Bearer ${resolved}` : resolved;
|
|
2665
|
-
}
|
|
2666
|
-
if (!value.startsWith("$env:")) return value;
|
|
2667
|
-
const name = value.slice(5);
|
|
2668
|
-
if (name === "") return void 0;
|
|
2669
|
-
const resolved = env[name];
|
|
2670
|
-
return resolved === void 0 || resolved === "" ? void 0 : resolved;
|
|
2671
|
-
}
|
|
2672
|
-
/** 服务声明的全部 $env 变量名(启动横幅 WARNING 用)。 */
|
|
2758
|
+
return entry.bearer === true ? `Bearer ${raw}` : raw;
|
|
2759
|
+
}
|
|
2760
|
+
/** 服务声明的 env 钩子变量名(启动横幅 WARNING 用:声明而未设置)。 */
|
|
2673
2761
|
function collectEnvVarNames(service) {
|
|
2674
2762
|
const names = /* @__PURE__ */ new Set();
|
|
2675
2763
|
const headerSet = service.rewrite?.headerSet;
|
|
2676
2764
|
if (headerSet === void 0) return [];
|
|
2677
|
-
for (const
|
|
2678
|
-
const name = value.slice(5);
|
|
2679
|
-
if (name !== "") names.add(name);
|
|
2680
|
-
}
|
|
2765
|
+
for (const entry of Object.values(headerSet)) if (typeof entry === "object" && entry.args?.var !== void 0 && (service.hooks === void 0 || service.hooks === "env")) names.add(entry.args.var);
|
|
2681
2766
|
return [...names];
|
|
2682
2767
|
}
|
|
2683
2768
|
/** HTTP hop-by-hop / 传输层自管头(普通 HTTP 转发路径剥离;WS 路径另行挑选)。 */
|
|
@@ -2756,7 +2841,7 @@ function isWebSocketUpgradeRequest(headers) {
|
|
|
2756
2841
|
if (connection === void 0 || upgrade === void 0) return false;
|
|
2757
2842
|
return connection.split(",").map((t) => t.trim().toLowerCase()).includes("upgrade") && upgrade.trim().toLowerCase() === "websocket";
|
|
2758
2843
|
}
|
|
2759
|
-
function buildUpstreamRequest(service, req, env = process.env, secrets) {
|
|
2844
|
+
async function buildUpstreamRequest(service, req, env = process.env, secrets) {
|
|
2760
2845
|
const upstream = parseUpstreamUrl(service.upstream);
|
|
2761
2846
|
assertFramePathShape(req.path);
|
|
2762
2847
|
const split = splitQuery(req.path);
|
|
@@ -2820,7 +2905,11 @@ function buildUpstreamRequest(service, req, env = process.env, secrets) {
|
|
|
2820
2905
|
for (const name of service.rewrite?.headerRemove ?? []) delete headers[name];
|
|
2821
2906
|
const headerSet = service.rewrite?.headerSet;
|
|
2822
2907
|
if (headerSet !== void 0) for (const [name, value] of Object.entries(headerSet)) {
|
|
2823
|
-
const resolved =
|
|
2908
|
+
const resolved = await resolveHeaderEntry(value, {
|
|
2909
|
+
script: service.hooks,
|
|
2910
|
+
env,
|
|
2911
|
+
secrets
|
|
2912
|
+
});
|
|
2824
2913
|
if (resolved === void 0) continue;
|
|
2825
2914
|
headers[name] = resolved;
|
|
2826
2915
|
}
|
|
@@ -3160,7 +3249,7 @@ function mergeTimeouts(overrides) {
|
|
|
3160
3249
|
async function forwardRequest(ctx) {
|
|
3161
3250
|
let plan;
|
|
3162
3251
|
try {
|
|
3163
|
-
plan = buildUpstreamRequest(ctx.service, ctx.req, ctx.env ?? process.env, ctx.secrets);
|
|
3252
|
+
plan = await buildUpstreamRequest(ctx.service, ctx.req, ctx.env ?? process.env, ctx.secrets);
|
|
3164
3253
|
} catch (err) {
|
|
3165
3254
|
const code = err instanceof SecretMissingError ? ERROR_CODE.secret_missing : err instanceof PathNotOfferedError ? ERROR_CODE.path_not_offered : ERROR_CODE.protocol_error;
|
|
3166
3255
|
const message = err instanceof RewriteError || err instanceof SecretMissingError || err instanceof PathNotOfferedError ? err.message : "request rewrite failed";
|
|
@@ -6181,9 +6270,13 @@ var providers_default = {
|
|
|
6181
6270
|
"localPrefix": "/codex",
|
|
6182
6271
|
"upstreamPrefix": "/backend-api/codex"
|
|
6183
6272
|
}],
|
|
6184
|
-
"authHeader":
|
|
6273
|
+
"authHeader": {
|
|
6274
|
+
"hook": "authHeader",
|
|
6275
|
+
"bearer": true
|
|
6276
|
+
},
|
|
6185
6277
|
"notes": "ChatGPT Codex backend (Responses API). Auth reads ~/.codex/auth.json per request (codex CLI login state; no key needed). Point agents at OPENAI_BASE_URL=http://127.0.0.1:<port>/codex with wire_api=responses. WS clients: ws://127.0.0.1:<port>/codex/responses upgrades to wss upstream.",
|
|
6186
|
-
"source": "https://github.com/openai/codex (~/.codex/auth.json login state)"
|
|
6278
|
+
"source": "https://github.com/openai/codex (~/.codex/auth.json login state)",
|
|
6279
|
+
"hooks": "codex"
|
|
6187
6280
|
}
|
|
6188
6281
|
]
|
|
6189
6282
|
};
|
|
@@ -6994,7 +7087,7 @@ async function testServiceRoute(input) {
|
|
|
6994
7087
|
bodyLen: 0
|
|
6995
7088
|
};
|
|
6996
7089
|
try {
|
|
6997
|
-
const plan = buildUpstreamRequest(input.service, req, input.env, input.secrets);
|
|
7090
|
+
const plan = await buildUpstreamRequest(input.service, req, input.env, input.secrets);
|
|
6998
7091
|
const response = await fetchImpl(plan.url, {
|
|
6999
7092
|
method: "POST",
|
|
7000
7093
|
headers: plan.headers,
|
|
@@ -7457,7 +7550,20 @@ function presetToServiceInput(preset, input) {
|
|
|
7457
7550
|
})),
|
|
7458
7551
|
...preset.routes !== void 0 && preset.routes.length > 0 ? { routes: preset.routes } : {},
|
|
7459
7552
|
...input.port !== void 0 ? { defaultPort: input.port } : { defaultPort: preset.defaultPort },
|
|
7460
|
-
...
|
|
7553
|
+
...preset.hooks !== void 0 ? { hooks: preset.hooks } : {},
|
|
7554
|
+
...input.secretName !== void 0 ? {
|
|
7555
|
+
hooks: "secret",
|
|
7556
|
+
rewrite: { headerSet: { authorization: {
|
|
7557
|
+
hook: "authHeader",
|
|
7558
|
+
args: { name: input.secretName }
|
|
7559
|
+
} } }
|
|
7560
|
+
} : preset.authHeader !== void 0 ? { rewrite: { headerSet: { authorization: preset.authHeader } } } : keyEnv !== void 0 ? {
|
|
7561
|
+
hooks: "env",
|
|
7562
|
+
rewrite: { headerSet: { authorization: {
|
|
7563
|
+
hook: "authHeader",
|
|
7564
|
+
args: { var: keyEnv }
|
|
7565
|
+
} } }
|
|
7566
|
+
} : {}
|
|
7461
7567
|
};
|
|
7462
7568
|
const envHint = input.secretName === void 0 && keyEnv !== void 0 ? `export ${keyEnv}='Bearer <your-api-key>' (full header value; the provider injects it upstream)` : void 0;
|
|
7463
7569
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as resolveRelayUrls, t as loadConfig } from "./config-BlZnFqx7.mjs";
|
|
2
2
|
import { n as loadSdk, t as resolveHttpProxy } from "./proxy-D7VMUVEg.mjs";
|
|
3
|
-
import { t as loadSettings } from "./settings-
|
|
3
|
+
import { t as loadSettings } from "./settings-D1Ukj8bF.mjs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
//#region src/cli/commands/consumer/common.ts
|
|
6
6
|
function ctxOut(ctx) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as UsageError } from "./errors-BGtxPkM7.mjs";
|
|
2
2
|
import { n as resolveRelayUrls, t as loadConfig } from "./config-BlZnFqx7.mjs";
|
|
3
|
-
import { t as ProviderStore } from "./store-
|
|
4
|
-
import { t as loadSettings } from "./settings-
|
|
3
|
+
import { t as ProviderStore } from "./store-z7qZO_5S.mjs";
|
|
4
|
+
import { t as loadSettings } from "./settings-D1Ukj8bF.mjs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
//#region src/cli/commands/provider/common.ts
|
|
@@ -56,13 +56,24 @@ function parseMatchSpec(raw) {
|
|
|
56
56
|
value
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
/** "Name=value"(首个 = 分割;Name 小写化)。 */
|
|
60
59
|
function parseHeaderSetSpec(raw) {
|
|
61
60
|
const idx = raw.indexOf("=");
|
|
62
|
-
if (idx <= 0) throw new UsageError(`error: invalid --header-set value: ${raw} (expected <name>=<value
|
|
61
|
+
if (idx <= 0) throw new UsageError(`error: invalid --header-set value: ${raw} (expected <name>=<value> or <name>={"hook":...}; value is a literal or hook invocation JSON)`);
|
|
62
|
+
const name = raw.slice(0, idx).trim().toLowerCase();
|
|
63
|
+
const rawValue = raw.slice(idx + 1);
|
|
64
|
+
if (rawValue.startsWith("{")) try {
|
|
65
|
+
const parsed = JSON.parse(rawValue);
|
|
66
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.hook !== "string") throw new Error("not a hook invocation");
|
|
67
|
+
return {
|
|
68
|
+
name,
|
|
69
|
+
value: parsed
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
throw new UsageError(`error: invalid --header-set hook JSON for '${name}' (expected {"hook":"authHeader",...})`);
|
|
73
|
+
}
|
|
63
74
|
return {
|
|
64
|
-
name
|
|
65
|
-
value:
|
|
75
|
+
name,
|
|
76
|
+
value: rawValue
|
|
66
77
|
};
|
|
67
78
|
}
|
|
68
79
|
/** 各命令的 rewrite 组装(仅在存在任一重写选项时构造)。 */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as UsageError, r as reportCliError, t as CliError } from "./errors-BGtxPkM7.mjs";
|
|
2
2
|
import { n as parseArgv } from "./args-CSxXW9eD.mjs";
|
|
3
|
-
import { r as settingsPath, t as loadSettings } from "./settings-
|
|
3
|
+
import { r as settingsPath, t as loadSettings } from "./settings-D1Ukj8bF.mjs";
|
|
4
4
|
import { a as killHard, c as terminateGracefully, i as isAlive, l as writeLastStart, n as daemonDir, o as readLastStart, r as ensureDaemonDir, s as readPid, t as clearPid, u as writePid } from "./daemon-state-CYkiW3SA.mjs";
|
|
5
|
-
import { r as openStore } from "./common-
|
|
5
|
+
import { r as openStore } from "./common-DGktbuQT.mjs";
|
|
6
6
|
import { openSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { isAbsolute, resolve } from "node:path";
|
|
@@ -99,7 +99,7 @@ async function start(startArgv, home) {
|
|
|
99
99
|
process.on("exit", () => {
|
|
100
100
|
clearPid(home);
|
|
101
101
|
});
|
|
102
|
-
const { run: serveRun } = await import("./serve-
|
|
102
|
+
const { run: serveRun } = await import("./serve-CEmNAhX5.mjs");
|
|
103
103
|
return serveRun([...startArgv], { homedir: home });
|
|
104
104
|
}
|
|
105
105
|
/** 后台 pid(活性核验;陈旧 pidfile 就地清理返回 null)。 */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as UsageError } from "./errors-BGtxPkM7.mjs";
|
|
2
2
|
import { n as parseArgv } from "./args-CSxXW9eD.mjs";
|
|
3
|
-
import { d as removeKeyring, n as consumersRoot } from "./store-
|
|
4
|
-
import { n as ctxHomedir, r as ctxOut } from "./common-
|
|
3
|
+
import { d as removeKeyring, n as consumersRoot } from "./store-DUMRSQVn.mjs";
|
|
4
|
+
import { n as ctxHomedir, r as ctxOut } from "./common-BerjPDN0.mjs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
//#region src/cli/commands/consumer/forget.ts
|
|
7
7
|
async function run(argv, ctx = {}) {
|
|
@@ -235,6 +235,7 @@ const SERVICE_ENTRY_SCHEMA = z.strictObject({
|
|
|
235
235
|
name: z.string().min(1).max(256),
|
|
236
236
|
match: z.array(SERVICE_MATCH_SCHEMA).max(64),
|
|
237
237
|
defaultPort: z.number().int().min(1).max(65535),
|
|
238
|
+
hooks: z.string().optional(),
|
|
238
239
|
detail: SERVICE_DETAIL_SCHEMA.optional()
|
|
239
240
|
});
|
|
240
241
|
const GROUP_ENTRY_SCHEMA = z.strictObject({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as UsageError, r as reportCliError } from "./errors-BGtxPkM7.mjs";
|
|
2
2
|
import { n as parseArgv } from "./args-CSxXW9eD.mjs";
|
|
3
|
-
import { d as str, l as resolveDataDir, n as multi, r as openStore, s as parsePositiveInt } from "./common-
|
|
3
|
+
import { d as str, l as resolveDataDir, n as multi, r as openStore, s as parsePositiveInt } from "./common-DGktbuQT.mjs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
//#region src/cli/commands/provider/group.ts
|
|
6
6
|
const SPEC = {
|