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.
Files changed (44) hide show
  1. package/dist/ai-fly.js +8 -7
  2. package/dist/app/main.js +205 -99
  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-C4kj0IwL.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-CXTcFgoT.mjs → revoke-CA4VYdTU.mjs} +2 -2
  22. package/dist/{rewrite-BL3jNnFZ.mjs → rewrite-zdPePt_u.mjs} +41 -92
  23. package/dist/{route-test-DqKRThLp.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-FjBsmK5n.mjs → serve-CEmNAhX5.mjs} +2 -2
  31. package/dist/{serve-GRJthOwO.mjs → serve-CvGVQszo.mjs} +6 -6
  32. package/dist/{service-DnQhGK4m.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-DpQPO1eC.mjs → share-CNYqhoFY.mjs} +5 -5
  36. package/dist/{status-BaRyycxh.mjs → status-DlB1AHty.mjs} +3 -3
  37. package/dist/{status-Ce-lxy52.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-FjBsmK5n.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-C4kj0IwL.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-DnQhGK4m.mjs"))(rest)).with("group", () => lazy(() => import("./group-CjwGDch3.mjs"))(rest)).with("share", () => lazy(() => import("./share-DpQPO1eC.mjs"))(rest)).with("revoke", () => lazy(() => import("./revoke-CXTcFgoT.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-Ce-lxy52.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
@@ -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)).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(),
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
- /** 预填 authorization 头值模板(如 `$file:~/.codex/auth.json#.tokens.access_token?bearer`);
209
- * 优先级:显式 secretName > authHeader > keyEnv。 */
210
- 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(),
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)).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(),
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
- * 解析 `$file:` 引用。path 支持 `~` 前缀(解析时展开);jsonPath 为 jq 风格
2588
- * 点径(`.tokens.access_token`,支持 `[n]` 数组下标);`?bearer` 后缀给非空
2589
- * 值拼 `Bearer ` 前缀。格式非法(缺 `#`)抛 SecretMissingError 同族的
2590
- * UsageError 语义由调用方决定——此处返回 null 交由上层按未命中处理。
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 parseFileRef(value) {
2593
- const body = value.slice(6);
2594
- const hash = body.indexOf("#");
2595
- if (hash <= 0) return null;
2596
- let spec = body.slice(hash + 1);
2597
- let bearer = false;
2598
- if (spec.endsWith("?bearer")) {
2599
- bearer = true;
2600
- spec = spec.slice(0, -7);
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 = readFileSync(path, "utf8");
2632
- } catch {
2633
- return;
2634
- }
2635
- let parsed;
2636
- try {
2637
- 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
+ });
2638
2755
  } catch {
2639
- return;
2756
+ throw new SecretMissingError();
2640
2757
  }
2641
- const value = evalJsonPath(parsed, jsonPath);
2642
- return typeof value === "string" && value !== "" ? value : void 0;
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 value of Object.values(headerSet)) if (value.startsWith("$env:")) {
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 = resolveHeaderValue(value, env, secrets);
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": "$file:~/.codex/auth.json#.tokens.access_token?bearer",
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
- ...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
+ } : {}
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-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-FjBsmK5n.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 = {