ai-fly 0.1.1 → 0.3.0-alpha.2

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 +48 -22
  2. package/dist/args-CSxXW9eD.mjs +69 -0
  3. package/dist/common-CgxCXeTt.mjs +83 -0
  4. package/dist/common-CqxqIg6D.mjs +86 -0
  5. package/dist/config-BlZnFqx7.mjs +59 -0
  6. package/dist/daemon-DklsEniO.mjs +291 -0
  7. package/dist/{forget-BEbNwPip.mjs → forget-B17zGwj4.mjs} +3 -2
  8. package/dist/{sdk-C6xDRbzg.mjs → frames-BDqvhB2Y.mjs} +1 -10
  9. package/dist/{group-Csy9VmFb.mjs → group-B96IKEDF.mjs} +24 -4
  10. package/dist/{import-BVAFw5Tx.mjs → import-tilxdbTH.mjs} +8 -6
  11. package/dist/{join-E1_-Rt-7.mjs → join-DsKUAibH.mjs} +7 -5
  12. package/dist/{join-B9op0juF.mjs → join-hyqiwEek.mjs} +3 -3
  13. package/dist/{key-CK77IYXp.mjs → key-BiEp995L.mjs} +2 -2
  14. package/dist/{key-Bt4EWiuO.mjs → key-CNufbB__.mjs} +4 -3
  15. package/dist/local-test-C3HGjGY3.mjs +101 -0
  16. package/dist/models-dev-2UfQoIpQ.mjs +244 -0
  17. package/dist/{mux-xHl9Jrbi.mjs → mux-CS1zhF4W.mjs} +2 -2
  18. package/dist/{ports-BQukx7wS.mjs → ports-rRppuBgo.mjs} +3 -2
  19. package/dist/presets-C73CAMZ6.mjs +55 -0
  20. package/dist/proxy-D7VMUVEg.mjs +21 -0
  21. package/dist/{revoke-CyrOzc8t.mjs → revoke-CkpvK33Q.mjs} +5 -4
  22. package/dist/rewrite-xZHWnyJk.mjs +313 -0
  23. package/dist/route-test-Oew0CO1U.mjs +60 -0
  24. package/dist/rpc-contract-5qyWzT5t.mjs +638 -0
  25. package/dist/{run-Be8H-IRf.mjs → run-DbPXAm6h.mjs} +8 -5
  26. package/dist/{runtime-Dg4en1D8.mjs → runtime-D_nxUEjd.mjs} +3 -3
  27. package/dist/secret-SA37VND5.mjs +95 -0
  28. package/dist/secrets-BYI6ooTJ.mjs +113 -0
  29. package/dist/secrets-u19-8EyW.mjs +2 -0
  30. package/dist/{serve-DuyQMeJh.mjs → serve-Be4zdKVj.mjs} +14 -425
  31. package/dist/{serve-R03mDBwd.mjs → serve-DSBlodTF.mjs} +18 -6
  32. package/dist/service-B5uORhl3.mjs +232 -0
  33. package/dist/settings-C1Hb4zwd.mjs +34 -0
  34. package/dist/settings-DU0YqNDt.mjs +95 -0
  35. package/dist/{share-DGM2TOft.mjs → share-Cug0944N.mjs} +8 -5
  36. package/dist/{status-DRaKNvqz.mjs → status-B-j454u2.mjs} +7 -5
  37. package/dist/{status-W2OS7WB6.mjs → status-SkNsxGnk.mjs} +6 -4
  38. package/dist/{common-4VeA_3wo.mjs → store-BiTS2FTl.mjs} +3 -73
  39. package/dist/store-CrTUwskE.mjs +629 -0
  40. package/dist/test-Bc0OKBAi.mjs +64 -0
  41. package/package.json +1 -1
  42. package/dist/common-BULbPtYn.mjs +0 -1295
  43. package/dist/config-CO3WyW2H.mjs +0 -173
  44. package/dist/service-Cd4R47n4.mjs +0 -94
@@ -0,0 +1,244 @@
1
+ import { t as PRESET_SCHEMA } from "./rpc-contract-5qyWzT5t.mjs";
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { z } from "zod";
6
+ //#region presets/providers.json
7
+ var providers_default = {
8
+ version: 1,
9
+ providers: [
10
+ {
11
+ "id": "openai",
12
+ "label": "OpenAI",
13
+ "apiForm": "openai-completions",
14
+ "baseUrl": "https://api.openai.com",
15
+ "keyEnv": "OPENAI_API_KEY",
16
+ "defaultPort": 4300,
17
+ "matchDomains": ["api.openai.com"],
18
+ "routes": [{
19
+ "forms": ["openai-chat", "openai-responses"],
20
+ "localPrefix": "/v1",
21
+ "upstreamPrefix": "/v1"
22
+ }],
23
+ "notes": "Official OpenAI API: chat completions + responses under /v1. $env injection expects the full header value, e.g. OPENAI_API_KEY='Bearer <key>'.",
24
+ "source": "https://models.dev/api.json (@ai-sdk/openai default base URL)"
25
+ },
26
+ {
27
+ "id": "anthropic",
28
+ "label": "Anthropic (Claude)",
29
+ "apiForm": "anthropic-messages",
30
+ "baseUrl": "https://api.anthropic.com",
31
+ "keyEnv": "ANTHROPIC_API_KEY",
32
+ "defaultPort": 4301,
33
+ "matchDomains": ["api.anthropic.com"],
34
+ "routes": [{
35
+ "forms": ["anthropic"],
36
+ "localPrefix": "/v1",
37
+ "upstreamPrefix": "/v1"
38
+ }],
39
+ "notes": "Anthropic Messages API (Claude models). Also the protocol claude-code speaks natively.",
40
+ "source": "https://models.dev/api.json (@ai-sdk/anthropic default base URL)"
41
+ },
42
+ {
43
+ "id": "deepseek",
44
+ "label": "DeepSeek",
45
+ "apiForm": "openai-completions",
46
+ "baseUrl": "https://api.deepseek.com",
47
+ "keyEnv": "DEEPSEEK_API_KEY",
48
+ "defaultPort": 4304,
49
+ "matchDomains": ["api.deepseek.com"],
50
+ "routes": [{
51
+ "forms": ["openai-chat", "openai-responses"],
52
+ "localPrefix": "/v1",
53
+ "upstreamPrefix": "/v1"
54
+ }, {
55
+ "forms": ["anthropic"],
56
+ "localPrefix": "/anthropic",
57
+ "upstreamPrefix": "/anthropic"
58
+ }],
59
+ "notes": "OpenAI-compatible at /v1/* (chat completions + responses, codex-ready); Anthropic-compatible surface at /anthropic/* (per vendor docs).",
60
+ "source": "https://models.dev/api.json + https://api-docs.deepseek.com/guides/anthropic_api"
61
+ }
62
+ ]
63
+ };
64
+ //#endregion
65
+ //#region presets/models-dev.ts
66
+ /** models.dev api.json 地址(spec 点名)。 */
67
+ const MODELS_DEV_API_URL = "https://models.dev/api.json";
68
+ /** 缓存路径(默认 ~/.aifly/cache/models-dev.json;测试注入)。 */
69
+ function modelsDevCachePath(base = homedir()) {
70
+ return join(base, ".aifly", "cache", "models-dev.json");
71
+ }
72
+ const CURATED_FILE_SCHEMA = z.strictObject({
73
+ version: z.literal(1),
74
+ providers: z.array(PRESET_SCHEMA).min(1)
75
+ });
76
+ /** 精选预设(模块加载即校验;仓库内数据损坏应 fail-fast 而非静默降级)。 */
77
+ function loadCuratedPresets() {
78
+ return CURATED_FILE_SCHEMA.parse(providers_default).providers;
79
+ }
80
+ /** api.json 中 provider 级条目的最小投影(防御性:多余字段忽略)。 */
81
+ const API_JSON_PROVIDER_SCHEMA = z.object({
82
+ id: z.string().min(1).max(128),
83
+ name: z.string().max(256).optional(),
84
+ /** 显式 base URL(SDK 缺省时缺失——长尾只收录显式 api 的条目)。 */
85
+ api: z.string().max(2048).optional(),
86
+ /** 惯用 env 名列表(取首个)。 */
87
+ env: z.array(z.string().max(256)).max(16).optional(),
88
+ /** npm 包名(apiForm 归类依据)。 */
89
+ npm: z.string().max(256).optional(),
90
+ /** provider 级模型清单(模型/价格 spec:至少 id 与 cost.input/output;缺失视为未知价)。 */
91
+ models: z.record(z.string().min(1).max(256), z.object({
92
+ id: z.string().min(1).max(256).optional(),
93
+ name: z.string().max(512).optional(),
94
+ cost: z.object({
95
+ input: z.number().nonnegative().optional(),
96
+ output: z.number().nonnegative().optional()
97
+ }).optional()
98
+ }).passthrough()).optional()
99
+ });
100
+ const API_JSON_SCHEMA = z.record(z.string(), API_JSON_PROVIDER_SCHEMA);
101
+ /** 缓存文件形状:原始 api.json 文本 + 抓取时间。 */
102
+ const CACHE_FILE_SCHEMA = z.strictObject({
103
+ fetchedAt: z.number().int().min(0),
104
+ raw: z.string().min(2)
105
+ });
106
+ /**
107
+ * npm 包名 → apiForm 归类(spec:npm 含 openai-compatible→openai-completions、
108
+ * anthropic→anthropic-messages、google→gemini-native、其它归 openai-completions
109
+ * 并标 unverified)。
110
+ */
111
+ function classifyApiForm(npm) {
112
+ const pkg = npm ?? "";
113
+ if (pkg.includes("anthropic")) return {
114
+ apiForm: "anthropic-messages",
115
+ unverified: false
116
+ };
117
+ if (pkg.includes("google")) return {
118
+ apiForm: "gemini-native",
119
+ unverified: false
120
+ };
121
+ if (pkg.includes("openai")) return {
122
+ apiForm: "openai-completions",
123
+ unverified: false
124
+ };
125
+ return {
126
+ apiForm: "openai-completions",
127
+ unverified: true
128
+ };
129
+ }
130
+ /** 长尾 defaultPort:id 的 FNV-1a 哈希映射到 20000..64999(确定性;监听期冲突由端口回退兜底)。 */
131
+ function derivedPortFor(id) {
132
+ let hash = 2166136261;
133
+ for (const byte of Buffer.from(id, "utf8")) {
134
+ hash ^= byte;
135
+ hash = Math.imul(hash, 16777619) >>> 0;
136
+ }
137
+ return 2e4 + hash % 45e3;
138
+ }
139
+ /** 从 api.json 原始文本派生长尾预设(跳过无显式 api 的条目;排除精选集已覆盖的 id)。 */
140
+ function deriveModelsDevPresets(raw, curatedIds) {
141
+ const parsed = JSON.parse(raw);
142
+ const providers = API_JSON_SCHEMA.parse(parsed);
143
+ const out = [];
144
+ for (const [id, provider] of Object.entries(providers)) {
145
+ if (provider.api === void 0 || provider.api === "") continue;
146
+ if (curatedIds.has(id)) continue;
147
+ if (!/^https?:\/\//.test(provider.api)) continue;
148
+ const { apiForm, unverified } = classifyApiForm(provider.npm);
149
+ let host;
150
+ try {
151
+ host = new URL(provider.api).hostname;
152
+ } catch {
153
+ continue;
154
+ }
155
+ const keyEnv = provider.env?.[0];
156
+ out.push(PRESET_SCHEMA.parse({
157
+ id,
158
+ label: provider.name && provider.name !== "" ? provider.name : id,
159
+ apiForm,
160
+ baseUrl: provider.api,
161
+ ...keyEnv !== void 0 ? { keyEnv } : {},
162
+ defaultPort: derivedPortFor(id),
163
+ matchDomains: [host],
164
+ source: "models.dev",
165
+ ...unverified ? { unverified: true } : {}
166
+ }));
167
+ }
168
+ return out;
169
+ }
170
+ /** 读取并校验缓存(损坏/不存在返回 undefined)。 */
171
+ function readCache(cachePath) {
172
+ try {
173
+ return CACHE_FILE_SCHEMA.parse(JSON.parse(readFileSync(cachePath, "utf8")));
174
+ } catch {
175
+ return;
176
+ }
177
+ }
178
+ /** 原子写缓存(失败静默:缓存写失败不致命)。 */
179
+ function writeCache(cachePath, raw, fetchedAt) {
180
+ try {
181
+ mkdirSync(dirname(cachePath), { recursive: true });
182
+ const tmp = `${cachePath}.tmp-${process.pid.toString(36)}-${Date.now().toString(36)}`;
183
+ writeFileSync(tmp, `${JSON.stringify({
184
+ fetchedAt,
185
+ raw
186
+ }, void 0, 2)}\n`);
187
+ renameSync(tmp, cachePath);
188
+ } catch {}
189
+ }
190
+ /**
191
+ * 取原始 api.json(presets.models 与长尾派生共用的数据面):TTL 内直接回缓存;
192
+ * 过期/未命中先刷新,失败回退缓存并附错误说明(spec:缓存未命中或已过期时
193
+ * SHALL 先尝试刷新,失败回退缓存)。
194
+ */
195
+ async function fetchModelsDevRaw(opts = {}) {
196
+ const now = opts.now ?? Date.now;
197
+ const cachePath = opts.cachePath ?? modelsDevCachePath();
198
+ const doFetch = opts.fetchImpl ?? fetch;
199
+ const cached = readCache(cachePath);
200
+ if (!opts.force && cached !== void 0 && now() - cached.fetchedAt < 6048e5) return {
201
+ raw: cached.raw,
202
+ origin: "cache"
203
+ };
204
+ try {
205
+ const response = await doFetch(MODELS_DEV_API_URL, { headers: { accept: "application/json" } });
206
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
207
+ const raw = await response.text();
208
+ JSON.parse(raw);
209
+ writeCache(cachePath, raw, now());
210
+ return {
211
+ raw,
212
+ origin: "fetch"
213
+ };
214
+ } catch (err) {
215
+ const message = err instanceof Error ? err.message : String(err);
216
+ if (cached !== void 0) return {
217
+ raw: cached.raw,
218
+ origin: "cache",
219
+ error: `fetch failed (${message}); serving cached copy`
220
+ };
221
+ return {
222
+ raw: void 0,
223
+ origin: "cache",
224
+ error: `fetch failed (${message}) and no cache available`
225
+ };
226
+ }
227
+ }
228
+ /** 拉取(或回退缓存)models.dev 长尾。失败且无可用缓存时返回 error 结果(不抛)。 */
229
+ async function fetchModelsDevPresets(curated, opts = {}) {
230
+ const result = await fetchModelsDevRaw(opts);
231
+ const curatedIds = new Set(curated.map((p) => p.id));
232
+ if (result.raw === void 0) return {
233
+ presets: [],
234
+ origin: "cache",
235
+ ...result.error !== void 0 ? { error: result.error } : {}
236
+ };
237
+ return {
238
+ presets: deriveModelsDevPresets(result.raw, curatedIds),
239
+ origin: result.origin,
240
+ ...result.error !== void 0 ? { error: result.error } : {}
241
+ };
242
+ }
243
+ //#endregion
244
+ export { loadCuratedPresets as n, modelsDevCachePath as r, fetchModelsDevPresets as t };
@@ -1,5 +1,5 @@
1
- import { i as randomZ32 } from "./config-CO3WyW2H.mjs";
2
- import { a as FRAME_TYPE, d as classifySchemaFailure, f as isKnownFrameType, i as FRAME_HEADER_SCHEMAS, n as ERROR_CODE, o as FRAME_TYPE_NAME } from "./sdk-C6xDRbzg.mjs";
1
+ import { s as randomZ32 } from "./rpc-contract-5qyWzT5t.mjs";
2
+ import { a as FRAME_TYPE_NAME, d as isKnownFrameType, i as FRAME_TYPE, r as FRAME_HEADER_SCHEMAS, t as ERROR_CODE, u as classifySchemaFailure } from "./frames-BDqvhB2Y.mjs";
3
3
  //#region src/wire/codec.ts
4
4
  const WIRE_MAGIC_PREFIX = "aifly";
5
5
  const WIRE_MAGIC = "aifly1";
@@ -1,6 +1,7 @@
1
1
  import { n as UsageError } from "./errors-BGtxPkM7.mjs";
2
- import { o as parseArgv } from "./config-CO3WyW2H.mjs";
3
- import { c as consumersRoot, n as ctxHomedir, p as listKeyrings, r as ctxOut, y as setPort } from "./common-4VeA_3wo.mjs";
2
+ import { n as parseArgv } from "./args-CSxXW9eD.mjs";
3
+ import { n as consumersRoot, p as setPort, s as listKeyrings } from "./store-BiTS2FTl.mjs";
4
+ import { n as ctxHomedir, r as ctxOut } from "./common-CgxCXeTt.mjs";
4
5
  import { i as validatePortArg, n as desiredPortFor } from "./ports-pI59MtmI.mjs";
5
6
  import { homedir } from "node:os";
6
7
  //#region src/cli/commands/consumer/ports.ts
@@ -0,0 +1,55 @@
1
+ import { n as UsageError, r as reportCliError } from "./errors-BGtxPkM7.mjs";
2
+ import { n as parseArgv } from "./args-CSxXW9eD.mjs";
3
+ import { t as loadSettings } from "./settings-C1Hb4zwd.mjs";
4
+ import { n as loadCuratedPresets, r as modelsDevCachePath, t as fetchModelsDevPresets } from "./models-dev-2UfQoIpQ.mjs";
5
+ import { homedir } from "node:os";
6
+ //#region src/cli/commands/provider/presets.ts
7
+ const SPEC = { json: { type: "boolean" } };
8
+ async function run(argv, ctx = {}) {
9
+ try {
10
+ const home = ctx.homedir ?? homedir();
11
+ const { options, positionals } = parseArgv(argv, SPEC, { homedir: home });
12
+ if (positionals.length > 1) throw new UsageError(`error: unexpected argument '${positionals[1]}'`);
13
+ const query = (positionals[0] ?? "").toLowerCase();
14
+ const curated = loadCuratedPresets();
15
+ const settings = loadSettings(home);
16
+ let longTail = [];
17
+ let note = "";
18
+ if (settings.modelsDevEnabled) try {
19
+ const result = await fetchModelsDevPresets(curated, { cachePath: modelsDevCachePath(home) });
20
+ if (result.error !== void 0) note = result.error;
21
+ longTail = result.presets;
22
+ } catch {
23
+ note = "models.dev expansion unavailable (offline?)";
24
+ }
25
+ else note = "models.dev expansion disabled in settings";
26
+ const featuredIds = new Set(curated.map((preset) => preset.id));
27
+ const longTailOnly = longTail.filter((preset) => !featuredIds.has(preset.id));
28
+ const all = [...curated, ...longTailOnly];
29
+ const hits = query === "" ? all : all.filter((preset) => `${preset.id} ${preset.label}`.toLowerCase().includes(query));
30
+ if (options.json === true) {
31
+ process.stdout.write(`${JSON.stringify(hits.map((p) => ({
32
+ id: p.id,
33
+ label: p.label,
34
+ baseUrl: p.baseUrl,
35
+ defaultPort: p.defaultPort
36
+ })), null, 2)}\n`);
37
+ return 0;
38
+ }
39
+ const out = (line) => {
40
+ process.stdout.write(`${line}\n`);
41
+ };
42
+ out(`presets (${hits.length}${query === "" ? "" : ` matching '${positionals[0]}'`})`);
43
+ for (const preset of hits) {
44
+ const badge = featuredIds.has(preset.id) ? "featured" : "long-tail";
45
+ out(` ${preset.id.padEnd(24)} ${preset.label} ${preset.baseUrl} :${preset.defaultPort} [${badge}]`);
46
+ }
47
+ if (note !== "") out(note);
48
+ out(`use: ai-fly service add <name> --preset <id>`);
49
+ return 0;
50
+ } catch (err) {
51
+ return reportCliError(err);
52
+ }
53
+ }
54
+ //#endregion
55
+ export { run };
@@ -0,0 +1,21 @@
1
+ import { n as UsageError } from "./errors-BGtxPkM7.mjs";
2
+ //#region src/sdk.ts
3
+ async function loadSdk() {
4
+ const candidate = await import("@jixo/opendweb-client-sdk");
5
+ if (typeof candidate.Fabric === "function") return candidate;
6
+ const def = candidate.default;
7
+ if (def !== void 0 && typeof def.Fabric === "function") return def;
8
+ throw new Error("error: @jixo/opendweb-client-sdk export shape unexpected (no Fabric via named or default interop)");
9
+ }
10
+ //#endregion
11
+ //#region src/cli/proxy.ts
12
+ function resolveHttpProxy(flag, env = process.env.AIFLY_PROXY) {
13
+ const spec = flag ?? env;
14
+ if (spec === void 0 || spec === "") return void 0;
15
+ if (spec === "none") return "none";
16
+ if (spec === "env") return "from-env";
17
+ if (!/^https?:\/\//i.test(spec)) throw new UsageError(`error: --proxy expects an http(s):// URL, 'env' or 'none' (got '${spec}')`);
18
+ return { url: spec };
19
+ }
20
+ //#endregion
21
+ export { loadSdk as n, resolveHttpProxy as t };
@@ -1,7 +1,8 @@
1
1
  import { n as UsageError, r as reportCliError } from "./errors-BGtxPkM7.mjs";
2
- import { o as parseArgv } from "./config-CO3WyW2H.mjs";
3
- import { d as str, l as resolveDataDir, u as resolvedRelayUrls } from "./common-BULbPtYn.mjs";
4
- import { t as openExistingFabric } from "./serve-DuyQMeJh.mjs";
2
+ import { n as parseArgv } from "./args-CSxXW9eD.mjs";
3
+ import { t as resolveHttpProxy } from "./proxy-D7VMUVEg.mjs";
4
+ import { d as str, l as resolveDataDir, u as resolvedRelayUrls } from "./common-CqxqIg6D.mjs";
5
+ import { t as openExistingFabric } from "./serve-Be4zdKVj.mjs";
5
6
  import { homedir } from "node:os";
6
7
  //#region src/cli/commands/provider/revoke.ts
7
8
  const SPEC = {
@@ -20,7 +21,7 @@ async function run(argv, ctx = {}) {
20
21
  if (endpointId === void 0 || endpointId === "") throw new UsageError("error: revoke requires an <endpointId> argument");
21
22
  if (positionals.length > 1) throw new UsageError(`error: unexpected argument '${positionals[1]}'`);
22
23
  const dataDir = resolveDataDir(str(options.data), home);
23
- fabric = await openExistingFabric(dataDir, resolvedRelayUrls(options, home));
24
+ fabric = await openExistingFabric(dataDir, resolvedRelayUrls(options, home), resolveHttpProxy(void 0));
24
25
  await fabric.revoke(endpointId);
25
26
  process.stdout.write(`member revoked: ${endpointId}\n`);
26
27
  return 0;
@@ -0,0 +1,313 @@
1
+ import { a as routeLocalPrefix } from "./rpc-contract-5qyWzT5t.mjs";
2
+ import { n as FORBIDDEN_REQ_HEADER_NAMES } from "./frames-BDqvhB2Y.mjs";
3
+ import { a as parseUpstreamUrl, c as matchRequestPath, o as expandUriTemplate, s as compileMatchPattern } from "./store-CrTUwskE.mjs";
4
+ const ENV_REF_PREFIX = "$env:";
5
+ /** $secret 引用前缀(密钥库名;与 $env 同样按引用整体掩码)。 */
6
+ const SECRET_REF_PREFIX = "$secret:";
7
+ function isEnvRef(value) {
8
+ return value.startsWith(ENV_REF_PREFIX);
9
+ }
10
+ function isSecretRef(value) {
11
+ return value.startsWith(SECRET_REF_PREFIX);
12
+ }
13
+ /** 引用型头值($env:/$secret:)——披露时统一掩码(名与值都不出)。 */
14
+ function isMaskedRef(value) {
15
+ return isEnvRef(value) || isSecretRef(value);
16
+ }
17
+ /** 前缀重写规则的披露形态:strip:/a 与 append:/b 的组合标记。 */
18
+ function prefixDisclosure(service) {
19
+ const rewrite = service.rewrite;
20
+ if (rewrite === void 0) return void 0;
21
+ const tokens = [];
22
+ if (rewrite.pathPrefixStrip !== void 0) tokens.push(`strip:${rewrite.pathPrefixStrip}`);
23
+ if (rewrite.pathPrefixAppend !== void 0) tokens.push(`append:${rewrite.pathPrefixAppend}`);
24
+ return tokens.length === 0 ? void 0 : tokens.join(" ");
25
+ }
26
+ /** 服务完整配置的脱敏披露($env 头值 -> ●,变量名不显示)。 */
27
+ function buildServiceDetail(service) {
28
+ const rewrite = {};
29
+ if (service.rewrite !== void 0) {
30
+ if (service.rewrite.hostHeader !== void 0) rewrite.host = service.rewrite.hostHeader;
31
+ const prefix = prefixDisclosure(service);
32
+ if (prefix !== void 0) rewrite.prefix = prefix;
33
+ if (service.rewrite.headerSet !== void 0) rewrite.headerSet = Object.entries(service.rewrite.headerSet).map(([name, value]) => ({
34
+ name,
35
+ value: isMaskedRef(value) ? "●" : value
36
+ }));
37
+ }
38
+ return {
39
+ upstream: service.upstream,
40
+ match: service.match.map((m) => ({
41
+ type: m.type,
42
+ value: m.value
43
+ })),
44
+ rewrite,
45
+ ...service.routes !== void 0 && service.routes.length > 0 ? { routes: service.routes.map((r) => r.mode === "pattern" ? {
46
+ forms: r.forms,
47
+ mode: "pattern",
48
+ matchPattern: r.matchPattern,
49
+ template: r.template
50
+ } : {
51
+ forms: r.forms,
52
+ localPrefix: routeLocalPrefix(r),
53
+ upstreamPrefix: r.upstreamPrefix ?? ""
54
+ }) } : {}
55
+ };
56
+ }
57
+ /** 目录服务条目(含 detail 脱敏披露)。 */
58
+ function buildServiceEntry(service) {
59
+ return {
60
+ serviceId: service.serviceId,
61
+ name: service.name,
62
+ match: service.match.map((m) => ({
63
+ type: m.type,
64
+ value: m.value
65
+ })),
66
+ defaultPort: service.defaultPort,
67
+ detail: buildServiceDetail(service)
68
+ };
69
+ }
70
+ /**
71
+ * detail 的 ASCII 展示形(CLI status --verbose 用;用户面文案码位 < 128,故 ●
72
+ * 在终端侧替换为 <hidden>——$env 与 $secret 掩码同形,无法区分,用中性词)。
73
+ */
74
+ function detailDisplayLines(detail) {
75
+ const lines = [`upstream: ${detail.upstream}`];
76
+ for (const m of detail.match) lines.push(`match: ${m.type} ${m.value}`);
77
+ for (const r of detail.routes ?? []) {
78
+ const forms = r.forms.length > 0 ? ` (${r.forms.join("+")})` : "";
79
+ if (r.mode === "pattern") lines.push(`route: ${r.matchPattern} => ${r.template}${forms}`);
80
+ else {
81
+ const to = r.upstreamPrefix === "" ? "(root)" : r.upstreamPrefix;
82
+ lines.push(`route: ${r.localPrefix} -> ${to}${forms}`);
83
+ }
84
+ }
85
+ if (detail.rewrite !== void 0) {
86
+ const r = detail.rewrite;
87
+ if (r.host !== void 0) lines.push(`host: ${r.host}`);
88
+ if (r.prefix !== void 0) lines.push(`prefix: ${r.prefix}`);
89
+ for (const h of r.headerSet ?? []) lines.push(`header-set: ${h.name}: ${h.value === "●" ? "<hidden>" : h.value}`);
90
+ }
91
+ return lines;
92
+ }
93
+ //#endregion
94
+ //#region src/provider/rewrite.ts
95
+ /** 拼接/断言失败(protocol_error 语义)。 */
96
+ var RewriteError = class extends Error {
97
+ constructor(message) {
98
+ super(message);
99
+ this.name = "RewriteError";
100
+ }
101
+ };
102
+ /**
103
+ * 服务声明了路由表但请求路径未命中任何标准前缀(path_not_offered 语义,
104
+ * 消费侧映射 404):只转发声明的 API 标准面——防 /user、/balance 等
105
+ * 个人信息端点被提供方凭据打穿(Owner 2026-09-10 裁决:路由表即白名单)。
106
+ */
107
+ var PathNotOfferedError = class extends Error {
108
+ constructor() {
109
+ super("path is not offered by this service");
110
+ this.name = "PathNotOfferedError";
111
+ }
112
+ };
113
+ /**
114
+ * `$secret:<name>` 引用未命中(secret_missing 语义):该请求以 ERROR(secret_missing)
115
+ * 拒绝。message 固定——MUST NOT 包含密钥名与值(错误帧会过网)。
116
+ */
117
+ var SecretMissingError = class extends Error {
118
+ constructor() {
119
+ super("referenced secret is missing");
120
+ this.name = "SecretMissingError";
121
+ }
122
+ };
123
+ /**
124
+ * 头值解析:literal 原样;`$secret:<name>` 优先于 `$env:` 判定——命中密钥库返回
125
+ * 完整值,未命中(含空名/空值/未注入密钥源)抛 SecretMissingError(不回退空值、
126
+ * 不省略该头);`$env:<VAR>` 语义保持——空串/未设置返回 undefined(= 省略该头)。
127
+ * 两者可并存于不同头(逐头独立解析)。
128
+ */
129
+ function resolveHeaderValue(value, env, secrets) {
130
+ if (value.startsWith("$secret:")) {
131
+ const name = value.slice(8);
132
+ const resolved = secrets?.(name);
133
+ if (resolved === void 0 || resolved === "") throw new SecretMissingError();
134
+ return resolved;
135
+ }
136
+ if (!value.startsWith("$env:")) return value;
137
+ const name = value.slice(5);
138
+ if (name === "") return void 0;
139
+ const resolved = env[name];
140
+ return resolved === void 0 || resolved === "" ? void 0 : resolved;
141
+ }
142
+ /** 服务声明的全部 $env 变量名(启动横幅 WARNING 用)。 */
143
+ function collectEnvVarNames(service) {
144
+ const names = /* @__PURE__ */ new Set();
145
+ const headerSet = service.rewrite?.headerSet;
146
+ if (headerSet === void 0) return [];
147
+ for (const value of Object.values(headerSet)) if (value.startsWith("$env:")) {
148
+ const name = value.slice(5);
149
+ if (name !== "") names.add(name);
150
+ }
151
+ return [...names];
152
+ }
153
+ /** HTTP hop-by-hop / 传输层自管头(普通 HTTP 转发路径剥离;WS 路径另行挑选)。 */
154
+ const HOP_BY_HOP_HEADER_NAMES = /* @__PURE__ */ new Set([
155
+ "connection",
156
+ "keep-alive",
157
+ "proxy-connection",
158
+ "transfer-encoding",
159
+ "te",
160
+ "trailer",
161
+ "upgrade"
162
+ ]);
163
+ /** 帧内头防御性剥离集合(wire schema 已拒绝;此处纵深防御)。 */
164
+ const DEFENSIVE_STRIP = /* @__PURE__ */ new Set([
165
+ ...FORBIDDEN_REQ_HEADER_NAMES,
166
+ ...HOP_BY_HOP_HEADER_NAMES,
167
+ "content-length"
168
+ ]);
169
+ /** 点段规范化(".", ".." 解析;不越出根,前缀断言兜底)。 */
170
+ function normalizeDotSegments(path) {
171
+ const out = [];
172
+ for (const seg of path.split("/")) {
173
+ if (seg === "." || seg === "") continue;
174
+ if (seg === "..") {
175
+ out.pop();
176
+ continue;
177
+ }
178
+ out.push(seg);
179
+ }
180
+ return `/${out.join("/")}`;
181
+ }
182
+ function splitQuery(raw) {
183
+ const idx = raw.indexOf("?");
184
+ return idx < 0 ? {
185
+ path: raw,
186
+ query: ""
187
+ } : {
188
+ path: raw.slice(0, idx),
189
+ query: raw.slice(idx + 1)
190
+ };
191
+ }
192
+ /** 查询值解码(畸形序列原样保留;模板会重新编码)。 */
193
+ function safeDecode(value) {
194
+ try {
195
+ return decodeURIComponent(value.replace(/\+/g, "%20"));
196
+ } catch {
197
+ return value;
198
+ }
199
+ }
200
+ /** 查询串 → 变量表(pattern 模板变量域;同名捕获组优先覆盖)。 */
201
+ function parseQueryVars(query) {
202
+ const vars = {};
203
+ if (query === "") return vars;
204
+ for (const pair of query.split("&")) {
205
+ const eq = pair.indexOf("=");
206
+ if (eq < 0) {
207
+ if (pair !== "") vars[pair] = "";
208
+ continue;
209
+ }
210
+ vars[pair.slice(0, eq)] = safeDecode(pair.slice(eq + 1));
211
+ }
212
+ return vars;
213
+ }
214
+ /** 帧内 path 的纵深防御检查(schema 层已拒;双保险)。 */
215
+ function assertFramePathShape(path) {
216
+ if (!path.startsWith("/") || path.startsWith("//") || path.startsWith("/\\")) throw new RewriteError("request path must start with a single '/'");
217
+ if (path.includes("\\")) throw new RewriteError("request path must not contain backslash");
218
+ const pathPart = splitQuery(path).path;
219
+ if (pathPart.includes("://")) throw new RewriteError("request path must not contain a scheme");
220
+ for (const seg of pathPart.split("/").slice(1)) if (seg === "." || seg === "..") throw new RewriteError("request path must not contain '.' or '..' segments");
221
+ }
222
+ /** connection 头 token 列表包含 upgrade 且 upgrade 头为 websocket(大小写不敏感)。 */
223
+ function isWebSocketUpgradeRequest(headers) {
224
+ const connection = headers["connection"];
225
+ const upgrade = headers["upgrade"];
226
+ if (connection === void 0 || upgrade === void 0) return false;
227
+ return connection.split(",").map((t) => t.trim().toLowerCase()).includes("upgrade") && upgrade.trim().toLowerCase() === "websocket";
228
+ }
229
+ function buildUpstreamRequest(service, req, env = process.env, secrets) {
230
+ const upstream = parseUpstreamUrl(service.upstream);
231
+ assertFramePathShape(req.path);
232
+ const split = splitQuery(req.path);
233
+ let requestPath = split.path;
234
+ let query = split.query;
235
+ if (service.routes !== void 0 && service.routes.length > 0) {
236
+ let matched = false;
237
+ for (const route of service.routes) {
238
+ if (route.mode === "pattern") {
239
+ const groups = matchRequestPath(compileMatchPattern(route.matchPattern), requestPath, query);
240
+ if (groups === null) continue;
241
+ const vars = {
242
+ ...parseQueryVars(query),
243
+ ...groups
244
+ };
245
+ const assembled = expandUriTemplate(route.template, vars);
246
+ const qIdx = assembled.indexOf("?");
247
+ if (qIdx >= 0) {
248
+ requestPath = assembled.slice(0, qIdx);
249
+ query = assembled.slice(qIdx + 1);
250
+ } else requestPath = assembled;
251
+ matched = true;
252
+ break;
253
+ }
254
+ const local = routeLocalPrefix(route);
255
+ if (!(requestPath === local || requestPath.startsWith(local + "/"))) continue;
256
+ const rest = requestPath.slice(local.length);
257
+ const up = route.upstreamPrefix ?? "";
258
+ requestPath = rest === "" ? up === "" ? "/" : up : `${up}${rest}`;
259
+ matched = true;
260
+ break;
261
+ }
262
+ if (!matched) throw new PathNotOfferedError();
263
+ }
264
+ const strip = service.rewrite?.pathPrefixStrip;
265
+ if (strip !== void 0 && (requestPath === strip || requestPath.startsWith(strip + "/"))) {
266
+ requestPath = requestPath.slice(strip.length);
267
+ if (requestPath === "") requestPath = "/";
268
+ }
269
+ const append = service.rewrite?.pathPrefixAppend;
270
+ if (append !== void 0) {
271
+ const appendNormalized = normalizeDotSegments(append).replace(/\/+$/, "");
272
+ if (appendNormalized !== "") requestPath = appendNormalized + (requestPath === "/" ? "/" : requestPath);
273
+ }
274
+ const baseNormalized = normalizeDotSegments(upstream.pathname === "" ? "/" : upstream.pathname);
275
+ const finalPath = normalizeDotSegments(baseNormalized === "/" ? requestPath : `${baseNormalized.replace(/\/+$/, "")}${requestPath}`);
276
+ let url;
277
+ try {
278
+ url = new URL(finalPath + (query === "" ? "" : `?${query}`), upstream);
279
+ } catch {
280
+ throw new RewriteError("cannot build upstream URL from service config and request path");
281
+ }
282
+ if (url.origin !== upstream.origin) throw new RewriteError(`upstream origin assertion failed (${url.origin} != ${upstream.origin})`);
283
+ if (!pathHasPrefix(finalPath, baseNormalized)) throw new RewriteError("upstream base path prefix assertion failed");
284
+ const headers = {};
285
+ for (const [name, value] of Object.entries(req.headers ?? {})) {
286
+ if (DEFENSIVE_STRIP.has(name)) continue;
287
+ headers[name] = value;
288
+ }
289
+ if (req.contentType !== void 0 && req.contentType !== "") headers["content-type"] = req.contentType;
290
+ for (const name of service.rewrite?.headerRemove ?? []) delete headers[name];
291
+ const headerSet = service.rewrite?.headerSet;
292
+ if (headerSet !== void 0) for (const [name, value] of Object.entries(headerSet)) {
293
+ const resolved = resolveHeaderValue(value, env, secrets);
294
+ if (resolved === void 0) continue;
295
+ headers[name] = resolved;
296
+ }
297
+ const host = service.rewrite?.hostHeader ?? upstream.host;
298
+ return {
299
+ url,
300
+ headers,
301
+ host,
302
+ isWebSocketUpgrade: isWebSocketUpgradeRequest(req.headers ?? {})
303
+ };
304
+ }
305
+ /** 段边界前缀判定:base 为根恒真;否则 final === base 或以 base/ 开头。 */
306
+ function pathHasPrefix(finalPath, basePath) {
307
+ if (basePath === "/" || basePath === "") return true;
308
+ if (finalPath === basePath) return true;
309
+ const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
310
+ return finalPath.startsWith(`${base}/`);
311
+ }
312
+ //#endregion
313
+ export { collectEnvVarNames as a, detailDisplayLines as c, buildUpstreamRequest as i, RewriteError as n, buildServiceDetail as o, SecretMissingError as r, buildServiceEntry as s, PathNotOfferedError as t };