ai-fly 0.4.0-alpha.3 → 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.
Files changed (44) hide show
  1. package/dist/ai-fly.js +8 -7
  2. package/dist/app/main.js +203 -147
  3. package/dist/{common-Cr7D8JyX.mjs → common-BerjPDN0.mjs} +1 -1
  4. package/dist/{common-BwKEJKOZ.mjs → common-DGktbuQT.mjs} +17 -6
  5. package/dist/{daemon-CuxLimRD.mjs → daemon-z7hZDf5X.mjs} +3 -3
  6. package/dist/{forget-oMwM-SsU.mjs → forget-zXXMWRPE.mjs} +2 -2
  7. package/dist/{frames-BDqvhB2Y.mjs → frames-BDz8buix.mjs} +1 -0
  8. package/dist/{group-CjwGDch3.mjs → group-DxTSU3w2.mjs} +1 -1
  9. package/dist/hook-CvAMGts4.mjs +228 -0
  10. package/dist/hooks-Da7bL31j.mjs +142 -0
  11. package/dist/{import-Zy3LL_PR.mjs → import-DVD6ODYu.mjs} +4 -4
  12. package/dist/{join-CdKKj7QS.mjs → join-BjpRt0Tj.mjs} +3 -3
  13. package/dist/{join-BJMcNGV-.mjs → join-BvMm1-Qz.mjs} +3 -3
  14. package/dist/{key-TP7lZeqW.mjs → key-BvFO4hCw.mjs} +1 -1
  15. package/dist/{key-BcNxtRJx.mjs → key-DHyfw_xd.mjs} +3 -3
  16. package/dist/{local-test-DbpiPEsD.mjs → local-test-CPOZODTW.mjs} +1 -1
  17. package/dist/{models-dev-7CclWuHF.mjs → models-dev-DhEP4LJH.mjs} +7 -3
  18. package/dist/{mux-BEXBL7rE.mjs → mux-BQ1w82tz.mjs} +2 -2
  19. package/dist/{ports-Csv91JOD.mjs → ports-Bs8NpX1c.mjs} +2 -2
  20. package/dist/{presets-DRO5N4Vm.mjs → presets-B339R3JU.mjs} +2 -2
  21. package/dist/{revoke-h1X8Xr_T.mjs → revoke-CA4VYdTU.mjs} +2 -2
  22. package/dist/{rewrite-WI8Ly8RX.mjs → rewrite-zdPePt_u.mjs} +41 -142
  23. package/dist/{route-test-D9Y4QmJn.mjs → route-test-D8se2bKr.mjs} +4 -4
  24. package/dist/{rpc-contract-CdiRZhUn.mjs → rpc-contract-eOK7IGzh.mjs} +21 -6
  25. package/dist/{run-W7DBz_bR.mjs → run-Ckodfe9X.mjs} +3 -3
  26. package/dist/{runtime-CGF9Sndm.mjs → runtime-CJWL72hJ.mjs} +3 -3
  27. package/dist/{secret-DKN75Vry.mjs → secret-BTq8J0Oj.mjs} +2 -2
  28. package/dist/{secrets-DFdwYE_V.mjs → secrets-BkwvIX0M.mjs} +2 -2
  29. package/dist/secrets-DtqxPHvY.mjs +2 -0
  30. package/dist/{serve-CRcqQR0j.mjs → serve-CEmNAhX5.mjs} +2 -2
  31. package/dist/{serve-B61VHcLi.mjs → serve-CvGVQszo.mjs} +6 -6
  32. package/dist/{service-CJq7aSR0.mjs → service-CBURGJU1.mjs} +19 -6
  33. package/dist/{settings-Dho5hk5Z.mjs → settings-BwDXTwUJ.mjs} +1 -1
  34. package/dist/{settings-BAa8I7Ww.mjs → settings-D1Ukj8bF.mjs} +2 -2
  35. package/dist/{share-D7ChVNte.mjs → share-CNYqhoFY.mjs} +5 -5
  36. package/dist/{status-BaRyycxh.mjs → status-DlB1AHty.mjs} +3 -3
  37. package/dist/{status-0NYOen8x.mjs → status-bV9YPNmT.mjs} +3 -3
  38. package/dist/{store-jmpkM28T.mjs → store-DUMRSQVn.mjs} +2 -2
  39. package/dist/{store-YUMvMCBg.mjs → store-z7qZO_5S.mjs} +9 -2
  40. package/dist/{test-Dxkkc-KN.mjs → test-DtOSl9gb.mjs} +2 -2
  41. package/package.json +1 -1
  42. package/webui/dist/assets/{index-0TnWjNwU.js → index-B52MRIjg.js} +3 -3
  43. package/webui/dist/index.html +1 -1
  44. 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>] [--route <local>=<up>[@forms]]...
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-CRcqQR0j.mjs"))(rest)).with("app", async () => {
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-CuxLimRD.mjs"))(rest)).with("presets", () => lazy(() => import("./presets-DRO5N4Vm.mjs"))(rest)).with("secret", () => lazy(() => import("./secret-DKN75Vry.mjs"))(rest)).with("settings", async () => (await import("./settings-Dho5hk5Z.mjs")).runAsSettings(rest, {})).with("relay", async () => (await import("./settings-Dho5hk5Z.mjs")).runAsRelay(rest, {})).with("test", () => lazy(() => import("./test-Dxkkc-KN.mjs"))(rest)).with("service", () => lazy(() => import("./service-CJq7aSR0.mjs"))(rest)).with("group", () => lazy(() => import("./group-CjwGDch3.mjs"))(rest)).with("share", () => lazy(() => import("./share-D7ChVNte.mjs"))(rest)).with("revoke", () => lazy(() => import("./revoke-h1X8Xr_T.mjs"))(rest)).with("join", () => lazy(() => import("./join-BJMcNGV-.mjs"))(rest)).with("import", () => lazy(() => import("./import-Zy3LL_PR.mjs"))(rest)).with("run", () => lazy(() => import("./run-W7DBz_bR.mjs"))(rest)).with("ports", () => lazy(() => import("./ports-Csv91JOD.mjs"))(rest)).with("forget", () => lazy(() => import("./forget-oMwM-SsU.mjs"))(rest)).with("key", () => {
81
- if (rest[0] === "add") return lazy(() => import("./key-BcNxtRJx.mjs"))(rest);
82
- return lazy(() => import("./key-TP7lZeqW.mjs"))(rest);
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-0NYOen8x.mjs"))(argv));
90
- if (wantConsumer) code = Math.max(code, await lazy(() => import("./status-BaRyycxh.mjs"))(argv));
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
@@ -60,7 +60,11 @@ const SERVICE_REWRITE_SCHEMA = z.strictObject({
60
60
  hostHeader: z.string().min(1).max(2048).optional(),
61
61
  pathPrefixStrip: z.string().min(1).max(2048).optional(),
62
62
  pathPrefixAppend: z.string().min(1).max(2048).optional(),
63
- headerSet: z.record(z.string().min(1).max(1024), z.string().max(8192)).optional(),
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(),
64
68
  headerRemove: z.array(z.string().min(1).max(1024)).max(32).optional()
65
69
  });
66
70
  const ROUTE_FORM_SCHEMA = z.enum([
@@ -108,6 +112,7 @@ const SERVICE_SCHEMA = z.strictObject({
108
112
  upstream: z.string().min(1).max(2048),
109
113
  rewrite: SERVICE_REWRITE_SCHEMA.optional(),
110
114
  routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional(),
115
+ hooks: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/).optional(),
111
116
  defaultPort: z.number().int().min(1).max(65535)
112
117
  });
113
118
  /** services.add 输入(defaultPort 缺省规则的校验在 store,同 CLI)。 */
@@ -117,7 +122,8 @@ const SERVICE_INPUT_SCHEMA = z.strictObject({
117
122
  match: z.array(SERVICE_MATCH_SCHEMA$1).min(1).max(64),
118
123
  defaultPort: z.number().int().min(1).max(65535).optional(),
119
124
  rewrite: SERVICE_REWRITE_SCHEMA.optional(),
120
- 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()
121
127
  });
122
128
  const GROUP_LIMITS_SCHEMA = z.strictObject({
123
129
  maxConcurrency: z.number().int().min(1).optional(),
@@ -155,7 +161,11 @@ const SERVICE_ENTRY_SCHEMA$1 = z.object({
155
161
  prefix: z.string().optional(),
156
162
  headerSet: z.array(z.strictObject({
157
163
  name: z.string(),
158
- 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
+ })])
159
169
  })).optional()
160
170
  }).optional(),
161
171
  /** 按标准路由披露(消费侧呈现各标准本地 base 与可用性判定)。 */
@@ -206,9 +216,14 @@ const PRESET_SCHEMA = z.strictObject({
206
216
  iconId: z.string().min(1).max(128).optional(),
207
217
  /** 惯用环境变量名(仅用于 $env 注入建议与文档;本地运行时模板无此字段)。 */
208
218
  keyEnv: z.string().min(1).max(256).optional(),
209
- /** 预填 authorization 头值模板(如 `$file:~/.codex/auth.json#.tokens.access_token?bearer`);
210
- * 优先级:显式 secretName > authHeader > keyEnv。 */
211
- authHeader: z.string().min(3).max(2048).optional(),
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(),
212
227
  /** 使用方本地端口建议(避开 <1024 特权段)。 */
213
228
  defaultPort: z.number().int().min(1024).max(65535),
214
229
  /** 官方域名集(exact/suffix 建议的生成源)。 */
@@ -839,7 +854,11 @@ const SERVICE_REWRITE_STORE_SCHEMA = z.strictObject({
839
854
  hostHeader: z.string().min(1).max(2048).optional(),
840
855
  pathPrefixStrip: z.string().min(1).max(2048).optional(),
841
856
  pathPrefixAppend: z.string().min(1).max(2048).optional(),
842
- headerSet: z.record(z.string().min(1).max(1024), z.string().max(8192)).optional(),
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(),
843
862
  headerRemove: z.array(z.string().min(1).max(1024)).max(32).optional()
844
863
  });
845
864
  /** 路径路由(M3-r7):按声明顺序命中的转发规则——prefix 模式(默认,
@@ -863,6 +882,8 @@ const SERVICE_STORE_SCHEMA = z.strictObject({
863
882
  match: z.array(SERVICE_MATCH_STORE_SCHEMA).max(64),
864
883
  upstream: z.string().min(1).max(2048),
865
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(),
866
887
  routes: z.array(SERVICE_ROUTE_STORE_SCHEMA).max(3).optional(),
867
888
  defaultPort: z.number().int().min(1).max(65535)
868
889
  });
@@ -1014,6 +1035,7 @@ var ProviderStore = class ProviderStore {
1014
1035
  match: input.match.map((m) => ({ ...m })),
1015
1036
  upstream: upstreamUrl.href,
1016
1037
  rewrite,
1038
+ ...input.hooks !== void 0 ? { hooks: input.hooks } : {},
1017
1039
  ...routes !== void 0 ? { routes } : {},
1018
1040
  defaultPort
1019
1041
  };
@@ -1613,6 +1635,7 @@ const SERVICE_ENTRY_SCHEMA = z.strictObject({
1613
1635
  name: z.string().min(1).max(256),
1614
1636
  match: z.array(SERVICE_MATCH_SCHEMA).max(64),
1615
1637
  defaultPort: z.number().int().min(1).max(65535),
1638
+ hooks: z.string().optional(),
1616
1639
  detail: SERVICE_DETAIL_SCHEMA.optional()
1617
1640
  });
1618
1641
  const GROUP_ENTRY_SCHEMA = z.strictObject({
@@ -2417,7 +2440,7 @@ function buildServiceDetail(service) {
2417
2440
  if (prefix !== void 0) rewrite.prefix = prefix;
2418
2441
  if (service.rewrite.headerSet !== void 0) rewrite.headerSet = Object.entries(service.rewrite.headerSet).map(([name, value]) => ({
2419
2442
  name,
2420
- value: isMaskedRef(value) ? "●" : value
2443
+ value: typeof value === "string" ? isMaskedRef(value) ? "●" : value : "●"
2421
2444
  }));
2422
2445
  }
2423
2446
  return {
@@ -2555,6 +2578,122 @@ var KeySessionIndex = class {
2555
2578
  }
2556
2579
  };
2557
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
2558
2697
  //#region src/provider/rewrite.ts
2559
2698
  /** 拼接/断言失败(protocol_error 语义)。 */
2560
2699
  var RewriteError = class extends Error {
@@ -2585,149 +2724,45 @@ var SecretMissingError = class extends Error {
2585
2724
  }
2586
2725
  };
2587
2726
  /**
2588
- * 解析 `$file:` 引用。path 支持 `~` 前缀(解析时展开);jsonPath 为 jq 风格
2589
- * 点径(`.tokens.access_token`,支持 `[n]` 数组下标);`?bearer` 后缀给非空
2590
- * 值拼 `Bearer ` 前缀。格式非法(缺 `#`)抛 SecretMissingError 同族的
2591
- * UsageError 语义由调用方决定——此处返回 null 交由上层按未命中处理。
2592
- */
2593
- function parseFileRef(value) {
2594
- const body = value.slice(6);
2595
- const hash = body.indexOf("#");
2596
- if (hash <= 0) return null;
2597
- let spec = body.slice(hash + 1);
2598
- let bearer = false;
2599
- if (spec.endsWith("?bearer")) {
2600
- bearer = true;
2601
- spec = spec.slice(0, -7);
2602
- }
2603
- if (spec === "" || !spec.startsWith(".")) return null;
2604
- return {
2605
- path: body.slice(0, hash),
2606
- jsonPath: spec,
2607
- bearer
2608
- };
2609
- }
2610
- /** jq 风格点径求值(`.a.b` / `.a[0].b`;起点须以 `.` 开头)。未命中返回 undefined。 */
2611
- function evalJsonPath(root, jsonPath) {
2612
- if (!jsonPath.startsWith(".")) return void 0;
2613
- let cur = root;
2614
- const segments = jsonPath.slice(1).split(/(?=\[)|\./).filter((s) => s !== "");
2615
- for (const seg of segments) {
2616
- if (cur === null || cur === void 0) return void 0;
2617
- const arr = /^\[(\d+)\]$/.exec(seg);
2618
- if (arr !== null) {
2619
- if (!Array.isArray(cur)) return void 0;
2620
- cur = cur[Number(arr[1])];
2621
- continue;
2622
- }
2623
- if (typeof cur !== "object") return void 0;
2624
- cur = cur[seg];
2625
- }
2626
- return cur;
2627
- }
2628
- function parseScriptRef(value) {
2629
- let body = value.slice(8);
2630
- let bearer = false;
2631
- if (body.endsWith("?bearer")) {
2632
- bearer = true;
2633
- body = body.slice(0, -7);
2634
- }
2635
- if (body === "") return null;
2636
- return {
2637
- path: body,
2638
- bearer
2639
- };
2640
- }
2641
- /**
2642
- * 默认脚本凭据源:createRequire 同步加载 CJS 模块并返回其导出——
2643
- * 函数导出由 resolveHeaderValue 每请求调用(值按调用计算,token 刷新即刻生效);
2644
- * 脚本文件本身的修改需重启 daemon(require 缓存,跨 Node/Bun/Deno 一致)。
2645
- * 信任模型与 $cmd 等价:脚本以 ai-fly 同权限执行宿主 IO(无 VM 隔离——
2646
- * node:vm 非安全边界且 Bun/Deno 支持残缺,Owner 裁决 2026-09-12 取简方案)。
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(零上游请求;不泄脚本路径与值)。
2647
2734
  */
2648
- const defaultScriptCredentialSource = (modulePath) => {
2649
- try {
2650
- return createRequire(import.meta.url)(modulePath);
2651
- } catch {
2652
- return;
2653
- }
2654
- };
2655
- /** 脚本导出归一:函数 / {default: fn} 取函数;其余视为无效(undefined)。 */
2656
- function scriptExportFn(mod) {
2657
- if (typeof mod === "function") return mod;
2658
- if (mod !== null && typeof mod === "object" && "default" in mod) {
2659
- const d = mod.default;
2660
- if (typeof d === "function") return d;
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();
2661
2744
  }
2662
- }
2663
- /** 默认文件凭据源:读文件 + JSON 解析 + 点径求值(仅字符串值;`~` 已在上层展开)。 */
2664
- const defaultFileCredentialSource = (path, jsonPath) => {
2665
2745
  let raw;
2666
2746
  try {
2667
- raw = readFileSync(path, "utf8");
2668
- } catch {
2669
- return;
2670
- }
2671
- let parsed;
2672
- try {
2673
- parsed = JSON.parse(raw);
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
+ });
2674
2755
  } catch {
2675
- return;
2676
- }
2677
- const value = evalJsonPath(parsed, jsonPath);
2678
- return typeof value === "string" && value !== "" ? value : void 0;
2679
- };
2680
- /**
2681
- * 头值解析:literal 原样;`$secret:<name>` 优先于 `$env:`/`$file:` 判定——命中
2682
- * 密钥库返回完整值,未命中(含空名/空值/未注入密钥源)抛 SecretMissingError
2683
- * (不回退空值、不省略该头);`$file:<path>#<json-path>[?bearer]` 每请求读盘
2684
- * (文件/键/空值未命中同抛 SecretMissingError;`?bearer` 拼前缀);
2685
- * `$env:<VAR>` 语义保持——空串/未设置返回 undefined(= 省略该头)。
2686
- * 各源可并存于不同头(逐头独立解析)。
2687
- */
2688
- function resolveHeaderValue(value, env, secrets, files = defaultFileCredentialSource, scripts = defaultScriptCredentialSource) {
2689
- if (value.startsWith("$secret:")) {
2690
- const name = value.slice(8);
2691
- const resolved = secrets?.(name);
2692
- if (resolved === void 0 || resolved === "") throw new SecretMissingError();
2693
- return resolved;
2694
- }
2695
- if (value.startsWith("$file:")) {
2696
- const ref = parseFileRef(value);
2697
- if (ref === null) throw new SecretMissingError();
2698
- const resolved = files(ref.path.startsWith("~/") ? join(homedir(), ref.path.slice(1)) : ref.path, ref.jsonPath);
2699
- if (resolved === void 0 || resolved === "") throw new SecretMissingError();
2700
- return ref.bearer ? `Bearer ${resolved}` : resolved;
2701
- }
2702
- if (value.startsWith("$script:")) {
2703
- const ref = parseScriptRef(value);
2704
- if (ref === null) throw new SecretMissingError();
2705
- const fn = scriptExportFn(scripts(ref.path.startsWith("~/") ? join(homedir(), ref.path.slice(1)) : ref.path));
2706
- if (fn === void 0) throw new SecretMissingError();
2707
- let resolved;
2708
- try {
2709
- resolved = fn({ homedir: homedir() });
2710
- } catch {
2711
- throw new SecretMissingError();
2712
- }
2713
- if (typeof resolved !== "string" || resolved === "") throw new SecretMissingError();
2714
- return ref.bearer ? `Bearer ${resolved}` : resolved;
2756
+ throw new SecretMissingError();
2715
2757
  }
2716
- if (!value.startsWith("$env:")) return value;
2717
- const name = value.slice(5);
2718
- if (name === "") return void 0;
2719
- const resolved = env[name];
2720
- return resolved === void 0 || resolved === "" ? void 0 : resolved;
2758
+ return entry.bearer === true ? `Bearer ${raw}` : raw;
2721
2759
  }
2722
- /** 服务声明的全部 $env 变量名(启动横幅 WARNING 用)。 */
2760
+ /** 服务声明的 env 钩子变量名(启动横幅 WARNING 用:声明而未设置)。 */
2723
2761
  function collectEnvVarNames(service) {
2724
2762
  const names = /* @__PURE__ */ new Set();
2725
2763
  const headerSet = service.rewrite?.headerSet;
2726
2764
  if (headerSet === void 0) return [];
2727
- for (const value of Object.values(headerSet)) if (value.startsWith("$env:")) {
2728
- const name = value.slice(5);
2729
- if (name !== "") names.add(name);
2730
- }
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);
2731
2766
  return [...names];
2732
2767
  }
2733
2768
  /** HTTP hop-by-hop / 传输层自管头(普通 HTTP 转发路径剥离;WS 路径另行挑选)。 */
@@ -2806,7 +2841,7 @@ function isWebSocketUpgradeRequest(headers) {
2806
2841
  if (connection === void 0 || upgrade === void 0) return false;
2807
2842
  return connection.split(",").map((t) => t.trim().toLowerCase()).includes("upgrade") && upgrade.trim().toLowerCase() === "websocket";
2808
2843
  }
2809
- function buildUpstreamRequest(service, req, env = process.env, secrets) {
2844
+ async function buildUpstreamRequest(service, req, env = process.env, secrets) {
2810
2845
  const upstream = parseUpstreamUrl(service.upstream);
2811
2846
  assertFramePathShape(req.path);
2812
2847
  const split = splitQuery(req.path);
@@ -2870,7 +2905,11 @@ function buildUpstreamRequest(service, req, env = process.env, secrets) {
2870
2905
  for (const name of service.rewrite?.headerRemove ?? []) delete headers[name];
2871
2906
  const headerSet = service.rewrite?.headerSet;
2872
2907
  if (headerSet !== void 0) for (const [name, value] of Object.entries(headerSet)) {
2873
- const resolved = resolveHeaderValue(value, env, secrets);
2908
+ const resolved = await resolveHeaderEntry(value, {
2909
+ script: service.hooks,
2910
+ env,
2911
+ secrets
2912
+ });
2874
2913
  if (resolved === void 0) continue;
2875
2914
  headers[name] = resolved;
2876
2915
  }
@@ -3210,7 +3249,7 @@ function mergeTimeouts(overrides) {
3210
3249
  async function forwardRequest(ctx) {
3211
3250
  let plan;
3212
3251
  try {
3213
- 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);
3214
3253
  } catch (err) {
3215
3254
  const code = err instanceof SecretMissingError ? ERROR_CODE.secret_missing : err instanceof PathNotOfferedError ? ERROR_CODE.path_not_offered : ERROR_CODE.protocol_error;
3216
3255
  const message = err instanceof RewriteError || err instanceof SecretMissingError || err instanceof PathNotOfferedError ? err.message : "request rewrite failed";
@@ -6231,9 +6270,13 @@ var providers_default = {
6231
6270
  "localPrefix": "/codex",
6232
6271
  "upstreamPrefix": "/backend-api/codex"
6233
6272
  }],
6234
- "authHeader": "$file:~/.codex/auth.json#.tokens.access_token?bearer",
6273
+ "authHeader": {
6274
+ "hook": "authHeader",
6275
+ "bearer": true
6276
+ },
6235
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.",
6236
- "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"
6237
6280
  }
6238
6281
  ]
6239
6282
  };
@@ -7044,7 +7087,7 @@ async function testServiceRoute(input) {
7044
7087
  bodyLen: 0
7045
7088
  };
7046
7089
  try {
7047
- const plan = buildUpstreamRequest(input.service, req, input.env, input.secrets);
7090
+ const plan = await buildUpstreamRequest(input.service, req, input.env, input.secrets);
7048
7091
  const response = await fetchImpl(plan.url, {
7049
7092
  method: "POST",
7050
7093
  headers: plan.headers,
@@ -7507,7 +7550,20 @@ function presetToServiceInput(preset, input) {
7507
7550
  })),
7508
7551
  ...preset.routes !== void 0 && preset.routes.length > 0 ? { routes: preset.routes } : {},
7509
7552
  ...input.port !== void 0 ? { defaultPort: input.port } : { defaultPort: preset.defaultPort },
7510
- ...input.secretName !== void 0 ? { rewrite: { headerSet: { authorization: `$secret:${input.secretName}` } } } : preset.authHeader !== void 0 ? { rewrite: { headerSet: { authorization: preset.authHeader } } } : keyEnv !== void 0 ? { rewrite: { headerSet: { authorization: `$env:${keyEnv}` } } } : {}
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
+ } : {}
7511
7567
  };
7512
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;
7513
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-BAa8I7Ww.mjs";
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-YUMvMCBg.mjs";
4
- import { t as loadSettings } from "./settings-BAa8I7Ww.mjs";
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>, value may be $env:VAR)`);
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: raw.slice(0, idx).trim().toLowerCase(),
65
- value: raw.slice(idx + 1)
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-BAa8I7Ww.mjs";
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-BwKEJKOZ.mjs";
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-CRcqQR0j.mjs");
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-jmpkM28T.mjs";
4
- import { n as ctxHomedir, r as ctxOut } from "./common-Cr7D8JyX.mjs";
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-BwKEJKOZ.mjs";
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 = {