ai-fly 0.0.0 → 0.1.0

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.
@@ -0,0 +1,1665 @@
1
+ import { a as FRAME_TYPE, c as REJECTED_CODE, l as RESP_META_HEADER_WHITELIST, n as ERROR_CODE, r as FORBIDDEN_REQ_HEADER_NAMES, t as loadSdk } from "./sdk-C6xDRbzg.mjs";
2
+ import { n as FabricWireAdapter, r as DEFAULT_BODY_CHUNK_BYTES, t as WireSession } from "./mux-xHl9Jrbi.mjs";
3
+ import { _ as expandUriTemplate, b as SECRET_NAME_SCHEMA, f as ProviderStore, g as parseUpstreamUrl, h as ensurePrivateDir, m as atomicWriteFileSync, p as StoreError, v as compileMatchPattern, x as routeLocalPrefix, y as matchRequestPath } from "./common-BULbPtYn.mjs";
4
+ import { appendFileSync, existsSync, readFileSync, readdirSync, watch } from "node:fs";
5
+ import "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { z } from "zod";
8
+ import WebSocket from "ws";
9
+ import { connect } from "node:net";
10
+ //#region src/provider/limits.ts
11
+ const QUOTA_DAY_SCHEMA = z.strictObject({
12
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
13
+ counts: z.record(z.string(), z.number().int().min(0))
14
+ });
15
+ var LimitEnforcer = class LimitEnforcer {
16
+ dataDir;
17
+ now;
18
+ /** 分组限额配置(reload 时整体替换)。 */
19
+ limitsByGroup = /* @__PURE__ */ new Map();
20
+ /** 分组在途计数。 */
21
+ inflight = /* @__PURE__ */ new Map();
22
+ quotaDate = "";
23
+ quotaCounts = /* @__PURE__ */ new Map();
24
+ constructor(opts) {
25
+ this.dataDir = opts.dataDir;
26
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
27
+ }
28
+ static quotaFilePath(dataDir) {
29
+ return join(dataDir, "quota-day.json");
30
+ }
31
+ /** 引擎在 store(重)加载后同步分组限额配置。 */
32
+ syncFromStore(store) {
33
+ this.limitsByGroup = new Map(store.listGroups().map((g) => [g.name, g.limits ? { ...g.limits } : {}]));
34
+ }
35
+ setGroupLimits(group, limits) {
36
+ this.limitsByGroup.set(group, { ...limits });
37
+ }
38
+ /** 拨号前检查+占用(原子):并发按分组计,日限按 keyId 计。 */
39
+ acquire(keyId, group) {
40
+ const limits = this.limitsByGroup.get(group) ?? {};
41
+ const current = this.inflight.get(group) ?? 0;
42
+ if (limits.maxConcurrency !== void 0 && current >= limits.maxConcurrency) return {
43
+ ok: false,
44
+ code: "rate_limited"
45
+ };
46
+ this.ensureQuotaLoaded();
47
+ const used = this.quotaCounts.get(keyId) ?? 0;
48
+ if (limits.dailyRequests !== void 0 && used >= limits.dailyRequests) return {
49
+ ok: false,
50
+ code: "quota_exceeded"
51
+ };
52
+ this.inflight.set(group, current + 1);
53
+ this.quotaCounts.set(keyId, used + 1);
54
+ this.persistQuota();
55
+ return { ok: true };
56
+ }
57
+ /** 请求终结时释放并发占用(幂等保护:下限 0)。 */
58
+ release(group) {
59
+ const current = this.inflight.get(group) ?? 0;
60
+ this.inflight.set(group, Math.max(0, current - 1));
61
+ }
62
+ inflightCount(group) {
63
+ return this.inflight.get(group) ?? 0;
64
+ }
65
+ dailyCount(keyId) {
66
+ this.ensureQuotaLoaded();
67
+ return this.quotaCounts.get(keyId) ?? 0;
68
+ }
69
+ today() {
70
+ return this.now().toISOString().slice(0, 10);
71
+ }
72
+ /** 加载/日界重置(懒加载:首次 acquire 或查询时)。 */
73
+ ensureQuotaLoaded() {
74
+ const today = this.today();
75
+ if (this.quotaDate === today) return;
76
+ let counts = /* @__PURE__ */ new Map();
77
+ const path = LimitEnforcer.quotaFilePath(this.dataDir);
78
+ if (existsSync(path)) try {
79
+ const parsed = QUOTA_DAY_SCHEMA.parse(JSON.parse(readFileSync(path, "utf8")));
80
+ if (parsed.date === today) counts = new Map(Object.entries(parsed.counts));
81
+ } catch {}
82
+ this.quotaDate = today;
83
+ this.quotaCounts = counts;
84
+ }
85
+ persistQuota() {
86
+ const payload = {
87
+ date: this.quotaDate,
88
+ counts: Object.fromEntries(this.quotaCounts)
89
+ };
90
+ atomicWriteFileSync(LimitEnforcer.quotaFilePath(this.dataDir), `${JSON.stringify(payload, null, 2)}\n`);
91
+ }
92
+ };
93
+ var UsageLog = class {
94
+ path;
95
+ dirReady = false;
96
+ constructor(dataDir) {
97
+ this.path = join(dataDir, "usage.jsonl");
98
+ }
99
+ append(record) {
100
+ if (!this.dirReady) {
101
+ ensurePrivateDir(dirname(this.path));
102
+ this.dirReady = true;
103
+ }
104
+ appendFileSync(this.path, `${JSON.stringify(record)}\n`, { mode: 384 });
105
+ }
106
+ pathOf() {
107
+ return this.path;
108
+ }
109
+ };
110
+ const ENV_REF_PREFIX = "$env:";
111
+ /** $secret 引用前缀(密钥库名;与 $env 同样按引用整体掩码)。 */
112
+ const SECRET_REF_PREFIX = "$secret:";
113
+ function isEnvRef(value) {
114
+ return value.startsWith(ENV_REF_PREFIX);
115
+ }
116
+ function isSecretRef(value) {
117
+ return value.startsWith(SECRET_REF_PREFIX);
118
+ }
119
+ /** 引用型头值($env:/$secret:)——披露时统一掩码(名与值都不出)。 */
120
+ function isMaskedRef(value) {
121
+ return isEnvRef(value) || isSecretRef(value);
122
+ }
123
+ /** 前缀重写规则的披露形态:strip:/a 与 append:/b 的组合标记。 */
124
+ function prefixDisclosure(service) {
125
+ const rewrite = service.rewrite;
126
+ if (rewrite === void 0) return void 0;
127
+ const tokens = [];
128
+ if (rewrite.pathPrefixStrip !== void 0) tokens.push(`strip:${rewrite.pathPrefixStrip}`);
129
+ if (rewrite.pathPrefixAppend !== void 0) tokens.push(`append:${rewrite.pathPrefixAppend}`);
130
+ return tokens.length === 0 ? void 0 : tokens.join(" ");
131
+ }
132
+ /** 服务完整配置的脱敏披露($env 头值 -> ●,变量名不显示)。 */
133
+ function buildServiceDetail(service) {
134
+ const rewrite = {};
135
+ if (service.rewrite !== void 0) {
136
+ if (service.rewrite.hostHeader !== void 0) rewrite.host = service.rewrite.hostHeader;
137
+ const prefix = prefixDisclosure(service);
138
+ if (prefix !== void 0) rewrite.prefix = prefix;
139
+ if (service.rewrite.headerSet !== void 0) rewrite.headerSet = Object.entries(service.rewrite.headerSet).map(([name, value]) => ({
140
+ name,
141
+ value: isMaskedRef(value) ? "●" : value
142
+ }));
143
+ }
144
+ return {
145
+ upstream: service.upstream,
146
+ match: service.match.map((m) => ({
147
+ type: m.type,
148
+ value: m.value
149
+ })),
150
+ rewrite,
151
+ ...service.routes !== void 0 && service.routes.length > 0 ? { routes: service.routes.map((r) => r.mode === "pattern" ? {
152
+ forms: r.forms,
153
+ mode: "pattern",
154
+ matchPattern: r.matchPattern,
155
+ template: r.template
156
+ } : {
157
+ forms: r.forms,
158
+ localPrefix: routeLocalPrefix(r),
159
+ upstreamPrefix: r.upstreamPrefix ?? ""
160
+ }) } : {}
161
+ };
162
+ }
163
+ /** 目录服务条目(含 detail 脱敏披露)。 */
164
+ function buildServiceEntry(service) {
165
+ return {
166
+ serviceId: service.serviceId,
167
+ name: service.name,
168
+ match: service.match.map((m) => ({
169
+ type: m.type,
170
+ value: m.value
171
+ })),
172
+ defaultPort: service.defaultPort,
173
+ detail: buildServiceDetail(service)
174
+ };
175
+ }
176
+ /**
177
+ * detail 的 ASCII 展示形(CLI status --verbose 用;用户面文案码位 < 128,故 ●
178
+ * 在终端侧替换为 <hidden>——$env 与 $secret 掩码同形,无法区分,用中性词)。
179
+ */
180
+ function detailDisplayLines(detail) {
181
+ const lines = [`upstream: ${detail.upstream}`];
182
+ for (const m of detail.match) lines.push(`match: ${m.type} ${m.value}`);
183
+ for (const r of detail.routes ?? []) {
184
+ const forms = r.forms.length > 0 ? ` (${r.forms.join("+")})` : "";
185
+ if (r.mode === "pattern") lines.push(`route: ${r.matchPattern} => ${r.template}${forms}`);
186
+ else {
187
+ const to = r.upstreamPrefix === "" ? "(root)" : r.upstreamPrefix;
188
+ lines.push(`route: ${r.localPrefix} -> ${to}${forms}`);
189
+ }
190
+ }
191
+ if (detail.rewrite !== void 0) {
192
+ const r = detail.rewrite;
193
+ if (r.host !== void 0) lines.push(`host: ${r.host}`);
194
+ if (r.prefix !== void 0) lines.push(`prefix: ${r.prefix}`);
195
+ for (const h of r.headerSet ?? []) lines.push(`header-set: ${h.name}: ${h.value === "●" ? "<hidden>" : h.value}`);
196
+ }
197
+ return lines;
198
+ }
199
+ //#endregion
200
+ //#region src/provider/auth.ts
201
+ function authDirectoryFromStore(store, over = {}) {
202
+ return {
203
+ alias: over.alias ?? store.alias ?? "provider",
204
+ relayUrls: over.relayUrls ?? [],
205
+ verifyKey: (key) => store.verifyKey(key),
206
+ groupLimits: (group) => store.getGroup(group)?.limits,
207
+ groupServices: (group) => store.groupServices(group).map(buildServiceEntry)
208
+ };
209
+ }
210
+ /** 钥环逐一校验(去重后;rejected 上限 64 对齐 wire schema)。 */
211
+ function evaluateKeyring(keys, dir) {
212
+ const seen = /* @__PURE__ */ new Set();
213
+ const valid = [];
214
+ const rejected = [];
215
+ for (const key of keys) {
216
+ if (seen.has(key)) continue;
217
+ seen.add(key);
218
+ const check = dir.verifyKey(key);
219
+ if (check.status === "valid") valid.push({
220
+ keyId: check.keyId,
221
+ group: check.group
222
+ });
223
+ else if (check.status === "revoked") {
224
+ if (rejected.length < 64) rejected.push({ code: REJECTED_CODE.key_revoked });
225
+ } else if (rejected.length < 64) rejected.push({ code: REJECTED_CODE.key_invalid });
226
+ }
227
+ return {
228
+ valid,
229
+ rejected
230
+ };
231
+ }
232
+ /** 每枚有效密钥 -> {keyId, group, limits, services[含 detail]} 视图。 */
233
+ function buildAuthOk(valid, dir, opts = {}) {
234
+ const groups = valid.map((grant) => {
235
+ const limits = dir.groupLimits(grant.group) ?? {};
236
+ return {
237
+ keyId: grant.keyId,
238
+ group: grant.group,
239
+ limits,
240
+ services: dir.groupServices(grant.group)
241
+ };
242
+ });
243
+ const header = {
244
+ v: 1,
245
+ alias: dir.alias,
246
+ relayUrls: [...dir.relayUrls],
247
+ groups
248
+ };
249
+ if (opts.refresh === true) header.refresh = true;
250
+ return header;
251
+ }
252
+ /** AUTH 帧决策(纯):全无效 -> err(调用方发送后断开)。 */
253
+ function handleAuthFrame(header, dir) {
254
+ const { valid, rejected } = evaluateKeyring(header.keys, dir);
255
+ if (valid.length === 0) return {
256
+ kind: "err",
257
+ header: {
258
+ v: 1,
259
+ code: "key_all_invalid",
260
+ message: "all presented keys are invalid or revoked"
261
+ }
262
+ };
263
+ const ok = buildAuthOk(valid, dir);
264
+ if (rejected.length > 0) ok.rejected = rejected;
265
+ return {
266
+ kind: "ok",
267
+ header: ok,
268
+ valid,
269
+ keys: [...new Set(header.keys)]
270
+ };
271
+ }
272
+ var KeySessionIndex = class {
273
+ byKey = /* @__PURE__ */ new Map();
274
+ /** 登记/更新会话的 keyId 归属(重复 AUTH 以最后一次为准)。 */
275
+ track(binding) {
276
+ this.untrack(binding);
277
+ for (const keyId of binding.keyIds) {
278
+ let set = this.byKey.get(keyId);
279
+ if (set === void 0) {
280
+ set = /* @__PURE__ */ new Set();
281
+ this.byKey.set(keyId, set);
282
+ }
283
+ set.add(binding);
284
+ }
285
+ }
286
+ untrack(binding) {
287
+ for (const [keyId, set] of this.byKey) {
288
+ set.delete(binding);
289
+ if (set.size === 0) this.byKey.delete(keyId);
290
+ }
291
+ }
292
+ sessionsWithKey(keyId) {
293
+ return [...this.byKey.get(keyId) ?? []];
294
+ }
295
+ size() {
296
+ let total = /* @__PURE__ */ new Set();
297
+ for (const set of this.byKey.values()) total = /* @__PURE__ */ new Set([...total, ...set]);
298
+ return total.size;
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region src/provider/rewrite.ts
303
+ /** 拼接/断言失败(protocol_error 语义)。 */
304
+ var RewriteError = class extends Error {
305
+ constructor(message) {
306
+ super(message);
307
+ this.name = "RewriteError";
308
+ }
309
+ };
310
+ /**
311
+ * 服务声明了路由表但请求路径未命中任何标准前缀(path_not_offered 语义,
312
+ * 消费侧映射 404):只转发声明的 API 标准面——防 /user、/balance 等
313
+ * 个人信息端点被提供方凭据打穿(Owner 2026-09-10 裁决:路由表即白名单)。
314
+ */
315
+ var PathNotOfferedError = class extends Error {
316
+ constructor() {
317
+ super("path is not offered by this service");
318
+ this.name = "PathNotOfferedError";
319
+ }
320
+ };
321
+ /**
322
+ * `$secret:<name>` 引用未命中(secret_missing 语义):该请求以 ERROR(secret_missing)
323
+ * 拒绝。message 固定——MUST NOT 包含密钥名与值(错误帧会过网)。
324
+ */
325
+ var SecretMissingError = class extends Error {
326
+ constructor() {
327
+ super("referenced secret is missing");
328
+ this.name = "SecretMissingError";
329
+ }
330
+ };
331
+ /**
332
+ * 头值解析:literal 原样;`$secret:<name>` 优先于 `$env:` 判定——命中密钥库返回
333
+ * 完整值,未命中(含空名/空值/未注入密钥源)抛 SecretMissingError(不回退空值、
334
+ * 不省略该头);`$env:<VAR>` 语义保持——空串/未设置返回 undefined(= 省略该头)。
335
+ * 两者可并存于不同头(逐头独立解析)。
336
+ */
337
+ function resolveHeaderValue(value, env, secrets) {
338
+ if (value.startsWith("$secret:")) {
339
+ const name = value.slice(8);
340
+ const resolved = secrets?.(name);
341
+ if (resolved === void 0 || resolved === "") throw new SecretMissingError();
342
+ return resolved;
343
+ }
344
+ if (!value.startsWith("$env:")) return value;
345
+ const name = value.slice(5);
346
+ if (name === "") return void 0;
347
+ const resolved = env[name];
348
+ return resolved === void 0 || resolved === "" ? void 0 : resolved;
349
+ }
350
+ /** 服务声明的全部 $env 变量名(启动横幅 WARNING 用)。 */
351
+ function collectEnvVarNames(service) {
352
+ const names = /* @__PURE__ */ new Set();
353
+ const headerSet = service.rewrite?.headerSet;
354
+ if (headerSet === void 0) return [];
355
+ for (const value of Object.values(headerSet)) if (value.startsWith("$env:")) {
356
+ const name = value.slice(5);
357
+ if (name !== "") names.add(name);
358
+ }
359
+ return [...names];
360
+ }
361
+ /** HTTP hop-by-hop / 传输层自管头(普通 HTTP 转发路径剥离;WS 路径另行挑选)。 */
362
+ const HOP_BY_HOP_HEADER_NAMES = /* @__PURE__ */ new Set([
363
+ "connection",
364
+ "keep-alive",
365
+ "proxy-connection",
366
+ "transfer-encoding",
367
+ "te",
368
+ "trailer",
369
+ "upgrade"
370
+ ]);
371
+ /** 帧内头防御性剥离集合(wire schema 已拒绝;此处纵深防御)。 */
372
+ const DEFENSIVE_STRIP = /* @__PURE__ */ new Set([
373
+ ...FORBIDDEN_REQ_HEADER_NAMES,
374
+ ...HOP_BY_HOP_HEADER_NAMES,
375
+ "content-length"
376
+ ]);
377
+ /** 点段规范化(".", ".." 解析;不越出根,前缀断言兜底)。 */
378
+ function normalizeDotSegments(path) {
379
+ const out = [];
380
+ for (const seg of path.split("/")) {
381
+ if (seg === "." || seg === "") continue;
382
+ if (seg === "..") {
383
+ out.pop();
384
+ continue;
385
+ }
386
+ out.push(seg);
387
+ }
388
+ return `/${out.join("/")}`;
389
+ }
390
+ function splitQuery(raw) {
391
+ const idx = raw.indexOf("?");
392
+ return idx < 0 ? {
393
+ path: raw,
394
+ query: ""
395
+ } : {
396
+ path: raw.slice(0, idx),
397
+ query: raw.slice(idx + 1)
398
+ };
399
+ }
400
+ /** 查询值解码(畸形序列原样保留;模板会重新编码)。 */
401
+ function safeDecode(value) {
402
+ try {
403
+ return decodeURIComponent(value.replace(/\+/g, "%20"));
404
+ } catch {
405
+ return value;
406
+ }
407
+ }
408
+ /** 查询串 → 变量表(pattern 模板变量域;同名捕获组优先覆盖)。 */
409
+ function parseQueryVars(query) {
410
+ const vars = {};
411
+ if (query === "") return vars;
412
+ for (const pair of query.split("&")) {
413
+ const eq = pair.indexOf("=");
414
+ if (eq < 0) {
415
+ if (pair !== "") vars[pair] = "";
416
+ continue;
417
+ }
418
+ vars[pair.slice(0, eq)] = safeDecode(pair.slice(eq + 1));
419
+ }
420
+ return vars;
421
+ }
422
+ /** 帧内 path 的纵深防御检查(schema 层已拒;双保险)。 */
423
+ function assertFramePathShape(path) {
424
+ if (!path.startsWith("/") || path.startsWith("//") || path.startsWith("/\\")) throw new RewriteError("request path must start with a single '/'");
425
+ if (path.includes("\\")) throw new RewriteError("request path must not contain backslash");
426
+ const pathPart = splitQuery(path).path;
427
+ if (pathPart.includes("://")) throw new RewriteError("request path must not contain a scheme");
428
+ for (const seg of pathPart.split("/").slice(1)) if (seg === "." || seg === "..") throw new RewriteError("request path must not contain '.' or '..' segments");
429
+ }
430
+ /** connection 头 token 列表包含 upgrade 且 upgrade 头为 websocket(大小写不敏感)。 */
431
+ function isWebSocketUpgradeRequest(headers) {
432
+ const connection = headers["connection"];
433
+ const upgrade = headers["upgrade"];
434
+ if (connection === void 0 || upgrade === void 0) return false;
435
+ return connection.split(",").map((t) => t.trim().toLowerCase()).includes("upgrade") && upgrade.trim().toLowerCase() === "websocket";
436
+ }
437
+ function buildUpstreamRequest(service, req, env = process.env, secrets) {
438
+ const upstream = parseUpstreamUrl(service.upstream);
439
+ assertFramePathShape(req.path);
440
+ const split = splitQuery(req.path);
441
+ let requestPath = split.path;
442
+ let query = split.query;
443
+ if (service.routes !== void 0 && service.routes.length > 0) {
444
+ let matched = false;
445
+ for (const route of service.routes) {
446
+ if (route.mode === "pattern") {
447
+ const groups = matchRequestPath(compileMatchPattern(route.matchPattern), requestPath, query);
448
+ if (groups === null) continue;
449
+ const vars = {
450
+ ...parseQueryVars(query),
451
+ ...groups
452
+ };
453
+ const assembled = expandUriTemplate(route.template, vars);
454
+ const qIdx = assembled.indexOf("?");
455
+ if (qIdx >= 0) {
456
+ requestPath = assembled.slice(0, qIdx);
457
+ query = assembled.slice(qIdx + 1);
458
+ } else requestPath = assembled;
459
+ matched = true;
460
+ break;
461
+ }
462
+ const local = routeLocalPrefix(route);
463
+ if (!(requestPath === local || requestPath.startsWith(local + "/"))) continue;
464
+ const rest = requestPath.slice(local.length);
465
+ const up = route.upstreamPrefix ?? "";
466
+ requestPath = rest === "" ? up === "" ? "/" : up : `${up}${rest}`;
467
+ matched = true;
468
+ break;
469
+ }
470
+ if (!matched) throw new PathNotOfferedError();
471
+ }
472
+ const strip = service.rewrite?.pathPrefixStrip;
473
+ if (strip !== void 0 && (requestPath === strip || requestPath.startsWith(strip + "/"))) {
474
+ requestPath = requestPath.slice(strip.length);
475
+ if (requestPath === "") requestPath = "/";
476
+ }
477
+ const append = service.rewrite?.pathPrefixAppend;
478
+ if (append !== void 0) {
479
+ const appendNormalized = normalizeDotSegments(append).replace(/\/+$/, "");
480
+ if (appendNormalized !== "") requestPath = appendNormalized + (requestPath === "/" ? "/" : requestPath);
481
+ }
482
+ const baseNormalized = normalizeDotSegments(upstream.pathname === "" ? "/" : upstream.pathname);
483
+ const finalPath = normalizeDotSegments(baseNormalized === "/" ? requestPath : `${baseNormalized.replace(/\/+$/, "")}${requestPath}`);
484
+ let url;
485
+ try {
486
+ url = new URL(finalPath + (query === "" ? "" : `?${query}`), upstream);
487
+ } catch {
488
+ throw new RewriteError("cannot build upstream URL from service config and request path");
489
+ }
490
+ if (url.origin !== upstream.origin) throw new RewriteError(`upstream origin assertion failed (${url.origin} != ${upstream.origin})`);
491
+ if (!pathHasPrefix(finalPath, baseNormalized)) throw new RewriteError("upstream base path prefix assertion failed");
492
+ const headers = {};
493
+ for (const [name, value] of Object.entries(req.headers ?? {})) {
494
+ if (DEFENSIVE_STRIP.has(name)) continue;
495
+ headers[name] = value;
496
+ }
497
+ if (req.contentType !== void 0 && req.contentType !== "") headers["content-type"] = req.contentType;
498
+ for (const name of service.rewrite?.headerRemove ?? []) delete headers[name];
499
+ const headerSet = service.rewrite?.headerSet;
500
+ if (headerSet !== void 0) for (const [name, value] of Object.entries(headerSet)) {
501
+ const resolved = resolveHeaderValue(value, env, secrets);
502
+ if (resolved === void 0) continue;
503
+ headers[name] = resolved;
504
+ }
505
+ const host = service.rewrite?.hostHeader ?? upstream.host;
506
+ return {
507
+ url,
508
+ headers,
509
+ host,
510
+ isWebSocketUpgrade: isWebSocketUpgradeRequest(req.headers ?? {})
511
+ };
512
+ }
513
+ /** 段边界前缀判定:base 为根恒真;否则 final === base 或以 base/ 开头。 */
514
+ function pathHasPrefix(finalPath, basePath) {
515
+ if (basePath === "/" || basePath === "") return true;
516
+ if (finalPath === basePath) return true;
517
+ const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
518
+ return finalPath.startsWith(`${base}/`);
519
+ }
520
+ //#endregion
521
+ //#region src/provider/ws-upstream.ts
522
+ /** http(s) URL -> ws(s) URL。 */
523
+ function toWebSocketUrl(url) {
524
+ return `${url.protocol === "https:" ? "wss" : "ws"}:${url.href.slice(url.protocol.length)}`;
525
+ }
526
+ function headerGet(headers, name) {
527
+ const value = headers[name];
528
+ if (value === void 0) return null;
529
+ return Array.isArray(value) ? value.join(", ") : value;
530
+ }
531
+ async function forwardWsUpgrade(ctx, plan, t) {
532
+ const { session, id } = ctx;
533
+ let settled = false;
534
+ let opened = false;
535
+ let downSeq = 0;
536
+ let bytes = 0;
537
+ let acceptHeader = null;
538
+ /** 完成回调(await 完成Promise 的 resolve;close 与非 101 中继任一触发)。 */
539
+ let completion;
540
+ let sendChain = Promise.resolve();
541
+ const recordUsage = (status) => {
542
+ ctx.onUsage?.({
543
+ ts: Date.now(),
544
+ keyId: ctx.keyId,
545
+ serviceId: ctx.service.serviceId,
546
+ status,
547
+ bytes
548
+ });
549
+ };
550
+ const enqueue = (task) => {
551
+ sendChain = sendChain.then(task).catch(() => void 0);
552
+ };
553
+ const sendErrorFrame = (code, message) => {
554
+ if (settled) return;
555
+ settled = true;
556
+ enqueue(async () => {
557
+ try {
558
+ await session.send(FRAME_TYPE.ERROR, {
559
+ id,
560
+ code,
561
+ message
562
+ });
563
+ } catch {}
564
+ });
565
+ };
566
+ session.suspendProviderIdle(id);
567
+ const pingTimer = t.pingMs > 0 ? setInterval(() => {
568
+ session.send(FRAME_TYPE.PING, { id }).catch(() => void 0);
569
+ }, t.pingMs) : null;
570
+ const cleanup = () => {
571
+ if (pingTimer !== null) clearInterval(pingTimer);
572
+ };
573
+ const teardown = (code) => {
574
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) code === void 0 ? ws.terminate() : ws.close(code);
575
+ };
576
+ const headers = {
577
+ ...plan.headers,
578
+ host: plan.host
579
+ };
580
+ delete headers["sec-websocket-extensions"];
581
+ delete headers["sec-websocket-key"];
582
+ delete headers["sec-websocket-version"];
583
+ delete headers["content-type"];
584
+ delete headers["content-length"];
585
+ const protocolHeader = headers["sec-websocket-protocol"];
586
+ delete headers["sec-websocket-protocol"];
587
+ const protocols = protocolHeader === void 0 ? [] : protocolHeader.split(",").map((p) => p.trim()).filter((p) => p !== "");
588
+ const ws = new WebSocket(toWebSocketUrl(plan.url), protocols, {
589
+ headers,
590
+ handshakeTimeout: t.connectMs,
591
+ perMessageDeflate: false
592
+ });
593
+ const relay = {
594
+ pushUp(data) {
595
+ if (ws.readyState !== WebSocket.OPEN) return;
596
+ bytes += data.length;
597
+ ws.send(data, { binary: true });
598
+ },
599
+ closeByPeer() {
600
+ settled = true;
601
+ cleanup();
602
+ teardown();
603
+ },
604
+ abort(code, reply) {
605
+ if (reply) sendErrorFrame(code, `websocket relay aborted (${code})`);
606
+ else settled = true;
607
+ cleanup();
608
+ teardown();
609
+ }
610
+ };
611
+ ctx.onWsRelay?.(relay);
612
+ const onExternalAbort = () => {
613
+ const { code, reply } = abortCodeOf(ctx.signal);
614
+ relay.abort(code, reply);
615
+ };
616
+ if (ctx.signal.aborted) onExternalAbort();
617
+ else ctx.signal.addEventListener("abort", onExternalAbort, { once: true });
618
+ ws.on("upgrade", (res) => {
619
+ acceptHeader = headerGet(res.headers, "sec-websocket-accept");
620
+ });
621
+ ws.on("unexpected-response", (_req, res) => {
622
+ if (settled) return;
623
+ settled = true;
624
+ const status = res.statusCode ?? 500;
625
+ const contentType = headerGet(res.headers, "content-type") ?? "";
626
+ enqueue(async () => {
627
+ const meta = {
628
+ id,
629
+ status,
630
+ contentType
631
+ };
632
+ const picked = pickResponseWhitelist((name) => headerGet(res.headers, name));
633
+ if (picked !== void 0) meta.headers = picked;
634
+ try {
635
+ await session.send(FRAME_TYPE.RESP_META, meta);
636
+ } catch {
637
+ return;
638
+ }
639
+ let seq = 0;
640
+ res.on("data", (chunk) => {
641
+ bytes += chunk.length;
642
+ enqueue(async () => {
643
+ for (const piece of splitBodyChunks(new Uint8Array(chunk))) {
644
+ await session.waitOutboundQueue(id);
645
+ await session.send(FRAME_TYPE.RESP_CHUNK, {
646
+ id,
647
+ seq
648
+ }, piece);
649
+ seq += 1;
650
+ }
651
+ });
652
+ });
653
+ res.on("end", () => {
654
+ enqueue(async () => {
655
+ try {
656
+ await session.send(FRAME_TYPE.RESP_END, { id });
657
+ } finally {
658
+ completion?.();
659
+ }
660
+ recordUsage(status);
661
+ });
662
+ });
663
+ res.on("error", () => {
664
+ enqueue(async () => {
665
+ try {
666
+ await session.send(FRAME_TYPE.ERROR, {
667
+ id,
668
+ code: ERROR_CODE.upstream_unreachable,
669
+ message: "upstream error response stream failed"
670
+ });
671
+ } catch {} finally {
672
+ completion?.();
673
+ }
674
+ });
675
+ });
676
+ res.resume();
677
+ });
678
+ });
679
+ ws.on("open", () => {
680
+ opened = true;
681
+ enqueue(async () => {
682
+ const meta = {
683
+ id,
684
+ status: 101,
685
+ contentType: ""
686
+ };
687
+ if (acceptHeader !== null) meta.headers = { "sec-websocket-accept": acceptHeader };
688
+ try {
689
+ await session.send(FRAME_TYPE.RESP_META, meta);
690
+ } catch {
691
+ return;
692
+ }
693
+ recordUsage(101);
694
+ });
695
+ });
696
+ ws.on("message", (data) => {
697
+ const payload = Buffer.isBuffer(data) ? data : Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
698
+ bytes += payload.length;
699
+ const pieces = splitBodyChunks(new Uint8Array(payload));
700
+ const startSeq = downSeq;
701
+ downSeq += pieces.length;
702
+ enqueue(async () => {
703
+ let seq = startSeq;
704
+ for (const piece of pieces) {
705
+ await session.waitOutboundQueue(id);
706
+ await session.send(FRAME_TYPE.DATA_DOWN, {
707
+ v: 1,
708
+ id,
709
+ seq
710
+ }, piece);
711
+ seq += 1;
712
+ }
713
+ });
714
+ });
715
+ ws.on("ping", () => {
716
+ session.send(FRAME_TYPE.PING, { id }).catch(() => void 0);
717
+ });
718
+ ws.on("close", (code) => {
719
+ cleanup();
720
+ if (settled) return;
721
+ settled = true;
722
+ enqueue(async () => {
723
+ const header = { id };
724
+ if (code >= 1e3 && code <= 65535) header.code = code;
725
+ try {
726
+ await session.send(FRAME_TYPE.CLOSE, header);
727
+ } catch {}
728
+ recordUsage(opened ? "ws-closed" : ERROR_CODE.upstream_unreachable);
729
+ });
730
+ });
731
+ ws.on("error", (err) => {
732
+ cleanup();
733
+ if (settled) return;
734
+ if (!opened) {
735
+ sendErrorFrame(ERROR_CODE.upstream_unreachable, `websocket handshake failed: ${err.message}`);
736
+ recordUsage(ERROR_CODE.upstream_unreachable);
737
+ return;
738
+ }
739
+ settled = true;
740
+ enqueue(async () => {
741
+ try {
742
+ await session.send(FRAME_TYPE.CLOSE, {
743
+ id,
744
+ code: 1011
745
+ });
746
+ } catch {}
747
+ recordUsage("ws-error");
748
+ });
749
+ });
750
+ await new Promise((resolve) => {
751
+ let completed = false;
752
+ const complete = () => {
753
+ if (completed) return;
754
+ completed = true;
755
+ resolve();
756
+ };
757
+ completion = complete;
758
+ ws.on("close", complete);
759
+ if (ws.readyState === WebSocket.CLOSED) complete();
760
+ });
761
+ completion = void 0;
762
+ cleanup();
763
+ }
764
+ //#endregion
765
+ //#region src/provider/upstream.ts
766
+ const DEFAULT_UPSTREAM_TIMEOUTS = {
767
+ connectMs: 1e4,
768
+ firstByteMs: 6e5,
769
+ stallMs: 12e4,
770
+ pingMs: 3e4
771
+ };
772
+ /** 引擎侧主动中止(abort reason):code = 回送 ERROR 码;reply=false 不回帧。 */
773
+ var UpstreamAbortError = class extends Error {
774
+ code;
775
+ reply;
776
+ constructor(code, reply = true) {
777
+ super(`upstream forward aborted: ${code}`);
778
+ this.name = "UpstreamAbortError";
779
+ this.code = code;
780
+ this.reply = reply;
781
+ }
782
+ };
783
+ /** 默认连接期探测:独立 TCP 连接探活(fetch 不暴露连接建立阶段;探测成功即销毁)。 */
784
+ function defaultProbeConnect(url, ms) {
785
+ return new Promise((resolve, reject) => {
786
+ const port = url.port !== "" ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
787
+ const socket = connect({
788
+ host: url.hostname,
789
+ port
790
+ });
791
+ let done = false;
792
+ const finish = (err) => {
793
+ if (done) return;
794
+ done = true;
795
+ socket.destroy();
796
+ if (err === void 0) resolve();
797
+ else reject(err);
798
+ };
799
+ socket.setTimeout(ms, () => finish(/* @__PURE__ */ new Error("connect timeout")));
800
+ socket.once("connect", () => finish());
801
+ socket.once("error", (err) => finish(err));
802
+ });
803
+ }
804
+ /** RESP_META 白名单头挑选(小写键)。 */
805
+ function pickResponseWhitelist(get) {
806
+ const out = {};
807
+ for (const name of RESP_META_HEADER_WHITELIST) {
808
+ const value = get(name);
809
+ if (value !== null) out[name] = value;
810
+ }
811
+ return Object.keys(out).length === 0 ? void 0 : out;
812
+ }
813
+ /** 大分片拆为 ≤ 上限的帧序列(RESP_CHUNK / DATA_DOWN 共用)。 */
814
+ function splitBodyChunks(data, limit = DEFAULT_BODY_CHUNK_BYTES) {
815
+ if (data.length <= limit) return [data];
816
+ const out = [];
817
+ for (let off = 0; off < data.length; off += limit) out.push(data.subarray(off, Math.min(off + limit, data.length)));
818
+ return out;
819
+ }
820
+ /** 信号 reason -> 回送码(缺省 aborted)。 */
821
+ function abortCodeOf(signal) {
822
+ const reason = signal.reason;
823
+ if (reason instanceof UpstreamAbortError) return {
824
+ code: reason.code,
825
+ reply: reason.reply
826
+ };
827
+ return {
828
+ code: ERROR_CODE.aborted,
829
+ reply: true
830
+ };
831
+ }
832
+ function mergeTimeouts(overrides) {
833
+ return {
834
+ ...DEFAULT_UPSTREAM_TIMEOUTS,
835
+ ...overrides
836
+ };
837
+ }
838
+ async function forwardRequest(ctx) {
839
+ let plan;
840
+ try {
841
+ plan = buildUpstreamRequest(ctx.service, ctx.req, ctx.env ?? process.env, ctx.secrets);
842
+ } catch (err) {
843
+ const code = err instanceof SecretMissingError ? ERROR_CODE.secret_missing : err instanceof PathNotOfferedError ? ERROR_CODE.path_not_offered : ERROR_CODE.protocol_error;
844
+ const message = err instanceof RewriteError || err instanceof SecretMissingError || err instanceof PathNotOfferedError ? err.message : "request rewrite failed";
845
+ await sendError(ctx.session, ctx.id, code, message);
846
+ ctx.onUsage?.({
847
+ ts: Date.now(),
848
+ keyId: ctx.keyId,
849
+ serviceId: ctx.service.serviceId,
850
+ status: code,
851
+ bytes: 0
852
+ });
853
+ return;
854
+ }
855
+ if (plan.isWebSocketUpgrade) {
856
+ await forwardWsUpgrade(ctx, plan, mergeTimeouts(ctx.timeouts));
857
+ return;
858
+ }
859
+ await forwardHttp(ctx, plan, mergeTimeouts(ctx.timeouts));
860
+ }
861
+ async function sendError(session, id, code, message) {
862
+ try {
863
+ await session.send(FRAME_TYPE.ERROR, {
864
+ id,
865
+ code,
866
+ message
867
+ });
868
+ } catch {}
869
+ }
870
+ async function forwardHttp(ctx, plan, t) {
871
+ const { session, id } = ctx;
872
+ let settled = false;
873
+ let bytes = 0;
874
+ const recordUsage = (status) => {
875
+ ctx.onUsage?.({
876
+ ts: Date.now(),
877
+ keyId: ctx.keyId,
878
+ serviceId: ctx.service.serviceId,
879
+ status,
880
+ bytes
881
+ });
882
+ };
883
+ const finishWithError = async (code, message) => {
884
+ if (settled) return;
885
+ settled = true;
886
+ await sendError(session, id, code, message);
887
+ recordUsage(code);
888
+ };
889
+ if ((ctx.req.method === "GET" || ctx.req.method === "HEAD") && ctx.body.length > 0) {
890
+ await finishWithError(ERROR_CODE.protocol_error, "request body not allowed for GET/HEAD");
891
+ return;
892
+ }
893
+ const probe = ctx.probeConnect ?? defaultProbeConnect;
894
+ try {
895
+ await probe(plan.url, t.connectMs);
896
+ } catch {
897
+ await finishWithError(ERROR_CODE.upstream_unreachable, "upstream connect failed or timed out");
898
+ return;
899
+ }
900
+ const ctrl = new AbortController();
901
+ let firstByteTimer = null;
902
+ let stallTimer = null;
903
+ let pingTimer = null;
904
+ let localAbortCode;
905
+ const stopPing = () => {
906
+ if (pingTimer !== null) {
907
+ clearInterval(pingTimer);
908
+ pingTimer = null;
909
+ }
910
+ };
911
+ const clearTimers = () => {
912
+ if (firstByteTimer !== null) {
913
+ clearTimeout(firstByteTimer);
914
+ firstByteTimer = null;
915
+ }
916
+ if (stallTimer !== null) {
917
+ clearTimeout(stallTimer);
918
+ stallTimer = null;
919
+ }
920
+ stopPing();
921
+ };
922
+ const onExternalAbort = () => {
923
+ ctrl.abort();
924
+ };
925
+ if (ctx.signal.aborted) onExternalAbort();
926
+ else ctx.signal.addEventListener("abort", onExternalAbort, { once: true });
927
+ session.suspendProviderIdle(id);
928
+ if (t.pingMs > 0) pingTimer = setInterval(() => {
929
+ session.send(FRAME_TYPE.PING, { id }).catch(() => void 0);
930
+ }, t.pingMs);
931
+ const abortWith = (code) => {
932
+ if (localAbortCode === void 0) localAbortCode = code;
933
+ ctrl.abort();
934
+ };
935
+ firstByteTimer = setTimeout(() => abortWith(ERROR_CODE.idle_timeout), t.firstByteMs);
936
+ const fetchFn = ctx.fetchImpl ?? fetch;
937
+ const headers = {
938
+ ...plan.headers,
939
+ host: plan.host
940
+ };
941
+ const init = {
942
+ method: ctx.req.method,
943
+ headers,
944
+ redirect: "manual",
945
+ signal: ctrl.signal
946
+ };
947
+ if (ctx.body.length > 0) init.body = ctx.body;
948
+ /** 中止分类:外部信号(ABORT/idle/断连)优先,其次本地超时码,否则网络失败。 */
949
+ const classifyAbort = () => {
950
+ if (!ctrl.signal.aborted) return void 0;
951
+ if (ctx.signal.aborted) return abortCodeOf(ctx.signal);
952
+ return {
953
+ code: localAbortCode ?? ERROR_CODE.aborted,
954
+ reply: true
955
+ };
956
+ };
957
+ let resp;
958
+ try {
959
+ resp = await fetchFn(plan.url, init);
960
+ } catch {
961
+ clearTimers();
962
+ const aborted = classifyAbort();
963
+ if (aborted !== void 0) {
964
+ if (aborted.reply) await finishWithError(aborted.code, `upstream request aborted (${aborted.code})`);
965
+ else settled = true;
966
+ return;
967
+ }
968
+ await finishWithError(ERROR_CODE.upstream_unreachable, "upstream request failed");
969
+ return;
970
+ }
971
+ clearTimers();
972
+ let reader;
973
+ try {
974
+ const metaHeader = {
975
+ id,
976
+ status: resp.status,
977
+ contentType: resp.headers.get("content-type") ?? ""
978
+ };
979
+ const picked = pickResponseWhitelist((name) => resp.headers.get(name));
980
+ if (picked !== void 0) metaHeader.headers = picked;
981
+ await session.send(FRAME_TYPE.RESP_META, metaHeader);
982
+ let seq = 0;
983
+ const armStall = () => {
984
+ if (stallTimer !== null) clearTimeout(stallTimer);
985
+ stallTimer = setTimeout(() => abortWith(ERROR_CODE.idle_timeout), t.stallMs);
986
+ };
987
+ armStall();
988
+ reader = resp.body?.getReader();
989
+ if (reader !== void 0) for (;;) {
990
+ const { done, value } = await reader.read();
991
+ if (done) break;
992
+ armStall();
993
+ for (const piece of splitBodyChunks(value)) {
994
+ await session.waitOutboundQueue(id);
995
+ await session.send(FRAME_TYPE.RESP_CHUNK, {
996
+ id,
997
+ seq
998
+ }, piece);
999
+ seq += 1;
1000
+ bytes += piece.length;
1001
+ }
1002
+ }
1003
+ if (stallTimer !== null) {
1004
+ clearTimeout(stallTimer);
1005
+ stallTimer = null;
1006
+ }
1007
+ if (ctx.signal.aborted) {
1008
+ const { code, reply } = abortCodeOf(ctx.signal);
1009
+ if (reply) await finishWithError(code, `upstream request aborted (${code})`);
1010
+ else settled = true;
1011
+ return;
1012
+ }
1013
+ await session.send(FRAME_TYPE.RESP_END, { id });
1014
+ settled = true;
1015
+ recordUsage(resp.status);
1016
+ } catch {
1017
+ if (stallTimer !== null) {
1018
+ clearTimeout(stallTimer);
1019
+ stallTimer = null;
1020
+ }
1021
+ const aborted = classifyAbort();
1022
+ if (aborted !== void 0) {
1023
+ if (aborted.reply) await finishWithError(aborted.code, `upstream request aborted (${aborted.code})`);
1024
+ else settled = true;
1025
+ return;
1026
+ }
1027
+ await finishWithError(ERROR_CODE.upstream_unreachable, "upstream stream failed");
1028
+ } finally {
1029
+ clearTimers();
1030
+ ctrl.abort();
1031
+ if (reader !== void 0) reader.cancel().catch(() => void 0);
1032
+ else resp.body?.cancel().catch(() => void 0);
1033
+ }
1034
+ }
1035
+ //#endregion
1036
+ //#region src/provider/engine.ts
1037
+ var ProviderEngine = class {
1038
+ fabric;
1039
+ dataDir;
1040
+ limitsEnforcer;
1041
+ usageLog;
1042
+ opts;
1043
+ storeRef;
1044
+ peers = /* @__PURE__ */ new Map();
1045
+ keyIndex = new KeySessionIndex();
1046
+ relayUrlsCache = [];
1047
+ unsubscribeFabric = null;
1048
+ stopped = false;
1049
+ constructor(deps) {
1050
+ this.fabric = deps.fabric;
1051
+ this.storeRef = deps.store;
1052
+ this.dataDir = deps.dataDir;
1053
+ this.limitsEnforcer = deps.limits ?? new LimitEnforcer({ dataDir: deps.dataDir });
1054
+ this.limitsEnforcer.syncFromStore(deps.store);
1055
+ this.opts = deps.opts ?? {};
1056
+ this.usageLog = this.opts.logUsage === true ? new UsageLog(deps.dataDir) : void 0;
1057
+ }
1058
+ get store() {
1059
+ return this.storeRef;
1060
+ }
1061
+ get limits() {
1062
+ return this.limitsEnforcer;
1063
+ }
1064
+ /** 订阅 fabric 事件并初始化 relay 缓存(幂等)。 */
1065
+ async start() {
1066
+ if (this.unsubscribeFabric !== null) return;
1067
+ await this.refreshRelayUrls();
1068
+ this.unsubscribeFabric = this.fabric.on((event) => {
1069
+ if (this.stopped) return;
1070
+ if (event.type === "peer-connected") this.addPeer(event.endpointId);
1071
+ else if (event.type === "peer-disconnected") this.removePeer(event.endpointId, "peer-disconnected");
1072
+ else if (event.type === "roster-updated") this.enforceMembership();
1073
+ else if (event.type === "relay-online" || event.type === "relay-offline") this.refreshRelayUrls().then(() => this.broadcastRefresh());
1074
+ });
1075
+ }
1076
+ async refreshRelayUrls() {
1077
+ try {
1078
+ const status = await this.fabric.relayStatus();
1079
+ this.relayUrlsCache = [...status.urls];
1080
+ } catch {}
1081
+ }
1082
+ sessionCount() {
1083
+ return this.peers.size;
1084
+ }
1085
+ peerIds() {
1086
+ return [...this.peers.keys()];
1087
+ }
1088
+ keySessionIndex() {
1089
+ return this.keyIndex;
1090
+ }
1091
+ alias() {
1092
+ return this.opts.alias ?? this.storeRef.alias ?? "provider";
1093
+ }
1094
+ directory() {
1095
+ return authDirectoryFromStore(this.storeRef, {
1096
+ alias: this.alias(),
1097
+ relayUrls: this.relayUrlsCache
1098
+ });
1099
+ }
1100
+ /** 服务变更/密钥撤销(CLI 写盘后由 watcher 触发):重读存储并刷新在线会话。 */
1101
+ async reloadStore() {
1102
+ const fresh = ProviderStore.open(this.dataDir);
1103
+ if (fresh.revision === this.storeRef.revision) return;
1104
+ this.storeRef = fresh;
1105
+ this.limitsEnforcer.syncFromStore(fresh);
1106
+ await this.broadcastRefresh();
1107
+ }
1108
+ /** 全量刷新:视图变化推送 refresh AUTH_OK;全钥失效断会话。 */
1109
+ async broadcastRefresh() {
1110
+ for (const ps of [...this.peers.values()]) await this.refreshSession(ps);
1111
+ }
1112
+ async refreshSession(ps) {
1113
+ if (!ps.session.authed) return;
1114
+ const dir = this.directory();
1115
+ const { valid } = evaluateKeyring(ps.keyring, dir);
1116
+ if (valid.length === 0) {
1117
+ this.removePeer(ps.peerId, "all keys invalid");
1118
+ return;
1119
+ }
1120
+ const header = buildAuthOk(valid, dir, { refresh: true });
1121
+ if (JSON.stringify(header) === ps.lastView) return;
1122
+ await ps.pushRefresh(header);
1123
+ ps.grants = valid;
1124
+ }
1125
+ addPeer(peerId) {
1126
+ if (this.stopped) return;
1127
+ if (this.peers.has(peerId)) this.removePeer(peerId, "replaced");
1128
+ this.peers.set(peerId, new ProviderPeerSession(this, peerId, this.fabric, (reason) => this.removePeer(peerId, reason)));
1129
+ }
1130
+ removePeer(peerId, reason) {
1131
+ const ps = this.peers.get(peerId);
1132
+ if (ps === void 0) return;
1133
+ this.peers.delete(peerId);
1134
+ this.keyIndex.untrack(ps);
1135
+ ps.disposeLocal(reason);
1136
+ }
1137
+ /** roster-updated 后复核在线 peer 成员资格:被 revoke 的成员即刻拆会话。 */
1138
+ async enforceMembership() {
1139
+ for (const peerId of [...this.peers.keys()]) try {
1140
+ if (!await this.fabric.isMember(peerId)) {
1141
+ this.removePeer(peerId, "revoked-from-roster");
1142
+ try {
1143
+ await this.fabric.disconnect(peerId);
1144
+ } catch {}
1145
+ }
1146
+ } catch {}
1147
+ }
1148
+ /** 用量记录(forward 回调;--logUsage 关闭时引擎侧不挂回调)。 */
1149
+ recordUsage(record) {
1150
+ this.usageLog?.append(record);
1151
+ }
1152
+ /** 幂等关闭:断开全部会话(fabric 本身由 serve 层 shutdown)。 */
1153
+ async shutdown() {
1154
+ if (this.stopped) return;
1155
+ this.stopped = true;
1156
+ this.unsubscribeFabric?.();
1157
+ this.unsubscribeFabric = null;
1158
+ for (const peerId of [...this.peers.keys()]) this.removePeer(peerId, "engine shutdown");
1159
+ }
1160
+ /** 组内是否含该服务(授权判定)。 */
1161
+ groupHasService(groupName, serviceId) {
1162
+ const group = this.storeRef.getGroup(groupName);
1163
+ return group !== void 0 && group.serviceIds.includes(serviceId);
1164
+ }
1165
+ };
1166
+ var ProviderPeerSession = class {
1167
+ peerId;
1168
+ session;
1169
+ adapter;
1170
+ engine;
1171
+ fabric;
1172
+ removeSelf;
1173
+ /** 最近一次 AUTH 呈交的钥环原文(仅内存;撤钥后重算用;MUST NOT 进日志)。 */
1174
+ keyring = [];
1175
+ grants = [];
1176
+ lastView = "";
1177
+ incoming = /* @__PURE__ */ new Map();
1178
+ active = /* @__PURE__ */ new Map();
1179
+ acquired = /* @__PURE__ */ new Map();
1180
+ disposed = false;
1181
+ constructor(engine, peerId, fabric, removeSelf) {
1182
+ this.engine = engine;
1183
+ this.peerId = peerId;
1184
+ this.fabric = fabric;
1185
+ this.removeSelf = removeSelf;
1186
+ this.adapter = new FabricWireAdapter(fabric, peerId);
1187
+ this.session = new WireSession({
1188
+ role: "provider",
1189
+ transport: this.adapter,
1190
+ hooks: {
1191
+ onFrame: (frame) => this.handleFrame(frame),
1192
+ onPoison: () => this.removeSelf("protocol-seq poisoned"),
1193
+ onIdleTimeout: (id) => this.handleIdleTimeout(id),
1194
+ onDisconnect: () => this.removeSelf("transport closed"),
1195
+ onTerminate: (id) => this.settle(id)
1196
+ }
1197
+ });
1198
+ }
1199
+ get keys() {
1200
+ return this.keyring;
1201
+ }
1202
+ get keyIds() {
1203
+ return new Set(this.grants.map((g) => g.keyId));
1204
+ }
1205
+ async pushRefresh(header) {
1206
+ try {
1207
+ await this.session.send(FRAME_TYPE.AUTH_OK, header);
1208
+ this.lastView = JSON.stringify(header);
1209
+ } catch {}
1210
+ }
1211
+ disconnect(reason) {
1212
+ this.session.dispose(reason);
1213
+ }
1214
+ handleFrame(frame) {
1215
+ switch (frame.type) {
1216
+ case FRAME_TYPE.AUTH:
1217
+ this.handleAuth(frame.header);
1218
+ return;
1219
+ case FRAME_TYPE.REQ:
1220
+ this.handleReq(frame.header, frame.body);
1221
+ return;
1222
+ case FRAME_TYPE.REQ_BODY:
1223
+ this.handleReqBody(frame.header, frame.body);
1224
+ return;
1225
+ case FRAME_TYPE.ABORT:
1226
+ this.handleAbort(frame.header);
1227
+ return;
1228
+ case FRAME_TYPE.DATA_UP:
1229
+ this.active.get(frame.header.id)?.ws?.pushUp(frame.body);
1230
+ return;
1231
+ case FRAME_TYPE.CLOSE:
1232
+ this.handleClose(frame.header);
1233
+ return;
1234
+ default: return;
1235
+ }
1236
+ }
1237
+ /** AUTH(重复 AUTH 以最后一次为准:钥环变更 = 重授权)。 */
1238
+ async handleAuth(header) {
1239
+ if (this.disposed) return;
1240
+ const decision = handleAuthFrame(header, this.engine.directory());
1241
+ if (decision.kind === "err") {
1242
+ try {
1243
+ await this.session.send(FRAME_TYPE.AUTH_ERR, decision.header);
1244
+ } catch {}
1245
+ this.disconnect("auth failed: all keys invalid");
1246
+ return;
1247
+ }
1248
+ this.keyring = decision.keys;
1249
+ this.grants = decision.valid;
1250
+ this.session.markAuthed();
1251
+ try {
1252
+ await this.session.send(FRAME_TYPE.AUTH_OK, decision.header);
1253
+ this.lastView = JSON.stringify(decision.header);
1254
+ this.engine.keySessionIndex().track(this);
1255
+ } catch {}
1256
+ }
1257
+ handleReq(header, body) {
1258
+ if (this.grants.length === 0) return;
1259
+ const service = this.engine.store.getService(header.serviceId);
1260
+ const grant = service === void 0 ? void 0 : this.grants.find((g) => this.engine.groupHasService(g.group, header.serviceId));
1261
+ if (service === void 0 || grant === void 0) {
1262
+ this.sendError(header.id, ERROR_CODE.unknown_service, "service not available");
1263
+ return;
1264
+ }
1265
+ const acquired = this.engine.limits.acquire(grant.keyId, grant.group);
1266
+ if (!acquired.ok) {
1267
+ this.sendError(header.id, acquired.code, acquired.code === "rate_limited" ? "group concurrency limit reached" : "daily request quota reached");
1268
+ return;
1269
+ }
1270
+ this.acquired.set(header.id, {
1271
+ keyId: grant.keyId,
1272
+ group: grant.group
1273
+ });
1274
+ if (header.bodyLen === body.length) {
1275
+ this.dispatch(header, body, grant.keyId, grant.group, service);
1276
+ return;
1277
+ }
1278
+ if (header.bodyLen < body.length) {
1279
+ this.sendError(header.id, ERROR_CODE.protocol_error, "request body length mismatch");
1280
+ return;
1281
+ }
1282
+ this.incoming.set(header.id, {
1283
+ req: header,
1284
+ parts: [body],
1285
+ keyId: grant.keyId,
1286
+ group: grant.group
1287
+ });
1288
+ }
1289
+ handleReqBody(header, body) {
1290
+ const inc = this.incoming.get(header.id);
1291
+ if (inc === void 0) return;
1292
+ inc.parts.push(body);
1293
+ const total = inc.parts.reduce((n, p) => n + p.length, 0);
1294
+ if (total > inc.req.bodyLen) {
1295
+ this.sendError(header.id, ERROR_CODE.protocol_error, "request body length mismatch");
1296
+ return;
1297
+ }
1298
+ if (!header.end) return;
1299
+ if (total !== inc.req.bodyLen) {
1300
+ this.sendError(header.id, ERROR_CODE.protocol_error, "request body length mismatch");
1301
+ return;
1302
+ }
1303
+ const merged = concatBytes(inc.parts);
1304
+ const { req, keyId, group } = inc;
1305
+ const service = this.engine.store.getService(req.serviceId);
1306
+ this.incoming.delete(header.id);
1307
+ if (service === void 0) {
1308
+ this.sendError(header.id, ERROR_CODE.unknown_service, "service not available");
1309
+ return;
1310
+ }
1311
+ this.dispatch(req, merged, keyId, group, service);
1312
+ }
1313
+ dispatch(req, body, keyId, group, service) {
1314
+ const ctrl = new AbortController();
1315
+ const act = {
1316
+ ctrl,
1317
+ keyId,
1318
+ group,
1319
+ ws: void 0
1320
+ };
1321
+ this.active.set(req.id, act);
1322
+ forwardRequest({
1323
+ session: this.session,
1324
+ id: req.id,
1325
+ service,
1326
+ req,
1327
+ body,
1328
+ signal: ctrl.signal,
1329
+ keyId,
1330
+ timeouts: this.engine.opts.timeouts,
1331
+ onUsage: this.engine.opts.logUsage === true ? (r) => this.engine.recordUsage(r) : void 0,
1332
+ env: this.engine.opts.env,
1333
+ secrets: this.engine.opts.secrets,
1334
+ onWsRelay: (relay) => {
1335
+ act.ws = relay;
1336
+ }
1337
+ }).catch(async () => {
1338
+ if (this.active.has(req.id)) this.sendError(req.id, ERROR_CODE.internal, "internal forward failure");
1339
+ });
1340
+ }
1341
+ handleAbort(header) {
1342
+ const act = this.active.get(header.id);
1343
+ if (act !== void 0) {
1344
+ act.ctrl.abort(new UpstreamAbortError(ERROR_CODE.aborted, true));
1345
+ return;
1346
+ }
1347
+ if (this.incoming.has(header.id)) this.sendError(header.id, ERROR_CODE.aborted, "request aborted by consumer");
1348
+ }
1349
+ handleClose(_header) {}
1350
+ handleIdleTimeout(id) {
1351
+ const act = this.active.get(id);
1352
+ if (act !== void 0) {
1353
+ act.ctrl.abort(new UpstreamAbortError(ERROR_CODE.idle_timeout, true));
1354
+ return;
1355
+ }
1356
+ if (this.incoming.has(id)) this.sendError(id, ERROR_CODE.idle_timeout, "request idle timeout");
1357
+ }
1358
+ /** id 终结清理(幂等):释放限额占用、中止上游(含 WS 关闭)、清表。 */
1359
+ settle(id) {
1360
+ const acquired = this.acquired.get(id);
1361
+ if (acquired !== void 0) {
1362
+ this.engine.limits.release(acquired.group);
1363
+ this.acquired.delete(id);
1364
+ }
1365
+ this.incoming.delete(id);
1366
+ const act = this.active.get(id);
1367
+ if (act !== void 0) {
1368
+ this.active.delete(id);
1369
+ act.ctrl.abort(new UpstreamAbortError(ERROR_CODE.internal, false));
1370
+ }
1371
+ }
1372
+ sendError(id, code, message) {
1373
+ this.session.send(FRAME_TYPE.ERROR, {
1374
+ id,
1375
+ code,
1376
+ message
1377
+ }).catch(() => void 0);
1378
+ }
1379
+ /** 本地善后(断连/毒化/引擎关闭):中止全部在途、释放限额、断开 fabric 会话。 */
1380
+ disposeLocal(reason) {
1381
+ if (this.disposed) return;
1382
+ this.disposed = true;
1383
+ for (const id of [...this.active.keys()]) this.settle(id);
1384
+ for (const id of [...this.acquired.keys()]) this.settle(id);
1385
+ this.adapter.dispose();
1386
+ this.session.dispose(reason);
1387
+ this.fabric.disconnect(this.peerId).catch(() => void 0);
1388
+ }
1389
+ };
1390
+ function concatBytes(parts) {
1391
+ const total = parts.reduce((n, p) => n + p.length, 0);
1392
+ const out = new Uint8Array(total);
1393
+ let off = 0;
1394
+ for (const p of parts) {
1395
+ out.set(p, off);
1396
+ off += p.length;
1397
+ }
1398
+ return out;
1399
+ }
1400
+ //#endregion
1401
+ //#region src/provider/secrets.ts
1402
+ /** 文件形状:{ version: 1, secrets: { [name]: { value, createdAt, updatedAt, bearerPrefix } } }。
1403
+ * 旧文件无 bearerPrefix 字段按 true 读入(zod default,写入时补齐)。 */
1404
+ const SECRETS_FILE_SCHEMA = z.strictObject({
1405
+ version: z.literal(1),
1406
+ secrets: z.record(SECRET_NAME_SCHEMA, z.strictObject({
1407
+ value: z.string().min(1).max(8192),
1408
+ createdAt: z.number().int().min(0),
1409
+ updatedAt: z.number().int().min(0),
1410
+ bearerPrefix: z.boolean().default(true)
1411
+ }))
1412
+ });
1413
+ const SECRETS_VALUE_MAX = 8192;
1414
+ var SecretsStore = class SecretsStore {
1415
+ dataDir;
1416
+ constructor(dataDir) {
1417
+ this.dataDir = dataDir;
1418
+ }
1419
+ static filePath(dataDir) {
1420
+ return join(dataDir, "secrets.json");
1421
+ }
1422
+ /** 打开(确保目录存在;文件不存在视为空库——首次 set 时落盘)。 */
1423
+ static open(dataDir) {
1424
+ ensurePrivateDir(dataDir);
1425
+ return new SecretsStore(dataDir);
1426
+ }
1427
+ /** 读盘 + 校验(不存在 → 空库;损坏/非法抛 StoreError(corrupt))。 */
1428
+ read() {
1429
+ const path = SecretsStore.filePath(this.dataDir);
1430
+ if (!existsSync(path)) return {
1431
+ version: 1,
1432
+ secrets: {}
1433
+ };
1434
+ let raw;
1435
+ try {
1436
+ raw = readFileSync(path, "utf8");
1437
+ } catch (err) {
1438
+ throw new StoreError("corrupt", `error: cannot read ${path}: ${err.message}`);
1439
+ }
1440
+ let parsed;
1441
+ try {
1442
+ parsed = JSON.parse(raw);
1443
+ } catch {
1444
+ throw new StoreError("corrupt", `error: ${path} is not valid JSON (fix or remove it manually)`);
1445
+ }
1446
+ const result = SECRETS_FILE_SCHEMA.safeParse(parsed);
1447
+ if (!result.success) throw new StoreError("corrupt", `error: ${path} failed validation: ${result.error.message}`);
1448
+ return result.data;
1449
+ }
1450
+ write(data) {
1451
+ atomicWriteFileSync(SecretsStore.filePath(this.dataDir), `${JSON.stringify(data, null, 2)}\n`);
1452
+ }
1453
+ /** 清单(名称/时间戳/bearer 开关;按名称排序稳定输出)。 */
1454
+ list() {
1455
+ const { secrets } = this.read();
1456
+ return Object.entries(secrets).map(([name, entry]) => ({
1457
+ name,
1458
+ createdAt: entry.createdAt,
1459
+ updatedAt: entry.updatedAt,
1460
+ bearerPrefix: entry.bearerPrefix
1461
+ })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1462
+ }
1463
+ /** 取值(未知名返回 undefined——由调用方决定失败语义,如 secret_missing)。 */
1464
+ get(name) {
1465
+ const entry = this.read().secrets[name];
1466
+ return entry === void 0 ? void 0 : entry.value;
1467
+ }
1468
+ /** 解析为最终注入头值:bearerPrefix 开启时前拼 "Bearer "(幂等:值已以
1469
+ * Bearer 开头则不再重复拼)。$secret:/test 一律走这里,不直接用 get()。 */
1470
+ resolve(name) {
1471
+ const entry = this.read().secrets[name];
1472
+ if (entry === void 0) return void 0;
1473
+ const bare = entry.value;
1474
+ return { headerValue: entry.bearerPrefix && !/^Bearer\s/i.test(bare) ? `Bearer ${bare}` : bare };
1475
+ }
1476
+ /** 新增/覆写(返回清单投影;createdAt 首次落定时固定,覆写只动 updatedAt)。 */
1477
+ set(name, value, opts) {
1478
+ if (!SECRET_NAME_SCHEMA.safeParse(name).success) throw new StoreError("invalid", "error: secret name must be 1..128 chars of lowercase letters, digits, dot, dash, underscore");
1479
+ if (value === "" || value.length > SECRETS_VALUE_MAX) throw new StoreError("invalid", `error: secret value must be 1..${SECRETS_VALUE_MAX} chars`);
1480
+ const data = this.read();
1481
+ const now = Date.now();
1482
+ const previous = data.secrets[name];
1483
+ const bearerPrefix = opts?.bearerPrefix ?? previous?.bearerPrefix ?? true;
1484
+ const entry = {
1485
+ value,
1486
+ createdAt: previous === void 0 ? now : previous.createdAt,
1487
+ updatedAt: now,
1488
+ bearerPrefix
1489
+ };
1490
+ data.secrets[name] = entry;
1491
+ this.write(data);
1492
+ return {
1493
+ name,
1494
+ createdAt: entry.createdAt,
1495
+ updatedAt: entry.updatedAt,
1496
+ bearerPrefix
1497
+ };
1498
+ }
1499
+ /** 删除(不存在抛 StoreError(not-found)——RPC 边界映射 NOT_FOUND)。 */
1500
+ remove(name) {
1501
+ const data = this.read();
1502
+ if (data.secrets[name] === void 0) throw new StoreError("not-found", `error: secret '${name}' not found`);
1503
+ delete data.secrets[name];
1504
+ this.write(data);
1505
+ }
1506
+ };
1507
+ //#endregion
1508
+ //#region src/provider/serve.ts
1509
+ function providerFabricDir(dataDir) {
1510
+ return join(dataDir, "fabric");
1511
+ }
1512
+ function fabricOptions(dataDir, relayUrls) {
1513
+ const opts = { dataDir: providerFabricDir(dataDir) };
1514
+ if (relayUrls !== void 0 && relayUrls.length > 0) opts.relay = {
1515
+ mode: "custom",
1516
+ urls: [relayUrls[0], ...relayUrls.slice(1)]
1517
+ };
1518
+ return opts;
1519
+ }
1520
+ function dirHasEntries(path) {
1521
+ return existsSync(path) && readdirSync(path).length > 0;
1522
+ }
1523
+ /**
1524
+ * 打开(或首启创建 root)数据目录下的 Fabric。已存在 fabric 数据则 open 复入
1525
+ * (EndpointId/名册不变),否则 createRoot。
1526
+ */
1527
+ async function openOrCreateFabric(dataDir, relayUrls) {
1528
+ const { Fabric } = await loadSdk();
1529
+ const opts = fabricOptions(dataDir, relayUrls);
1530
+ if (dirHasEntries(providerFabricDir(dataDir))) try {
1531
+ return await Fabric.open(opts);
1532
+ } catch (err) {
1533
+ return await Fabric.createRoot(opts).catch(() => Promise.reject(err instanceof Error ? err : new Error(String(err))));
1534
+ }
1535
+ try {
1536
+ return await Fabric.createRoot(opts);
1537
+ } catch (err) {
1538
+ return await Fabric.open(opts).catch(() => Promise.reject(err instanceof Error ? err : new Error(String(err))));
1539
+ }
1540
+ }
1541
+ /** 打开既有 fabric 身份(share/revoke/status 用;未初始化时抛错)。 */
1542
+ async function openExistingFabric(dataDir, relayUrls) {
1543
+ if (!dirHasEntries(providerFabricDir(dataDir))) throw new Error(`error: no fabric identity under ${dataDir}; run 'ai-fly serve --data <dir>' first`);
1544
+ const { Fabric } = await loadSdk();
1545
+ return Fabric.open(fabricOptions(dataDir, relayUrls));
1546
+ }
1547
+ /** 扫描空/未设置的 $env 变量(已声明但为空 -> WARNING 列表)。 */
1548
+ function findEmptyEnvRefs(services, env = process.env) {
1549
+ const out = [];
1550
+ for (const service of services) {
1551
+ const empty = collectEnvVarNames(service).filter((name) => {
1552
+ const value = env[name];
1553
+ return value === void 0 || value === "";
1554
+ });
1555
+ if (empty.length > 0) out.push({
1556
+ service: service.name,
1557
+ vars: empty
1558
+ });
1559
+ }
1560
+ return out;
1561
+ }
1562
+ /** 启动横幅(英文 ASCII;用户面文案码位 < 128)。 */
1563
+ function composeStartupBanner(input) {
1564
+ const lines = [];
1565
+ lines.push("ai-fly provider daemon");
1566
+ lines.push(` EndpointId : ${input.endpointId}`);
1567
+ lines.push(` FabricId : ${input.fabricIdHex}`);
1568
+ lines.push(` Data dir : ${input.dataDir}`);
1569
+ lines.push(` Alias : ${input.alias}`);
1570
+ lines.push(` Services : ${input.serviceCount}`);
1571
+ lines.push(` Groups : ${input.groupCount} (keys: ${input.activeKeyCount} active, ${input.revokedKeyCount} revoked)`);
1572
+ if (input.relayUrls.length > 0) lines.push(` Relay : ${input.relayMode} ${input.relayUrls.join(", ")}`);
1573
+ else lines.push(` Relay : ${input.relayMode} (none)`);
1574
+ for (const ref of input.emptyEnvRefs) for (const name of ref.vars) lines.push(` WARNING: $env variable '${name}' referenced by service '${ref.service}' is empty or unset`);
1575
+ lines.push("Ready. Waiting for consumers (Ctrl+C to stop).");
1576
+ return lines.join("\n");
1577
+ }
1578
+ async function startProviderDaemon(opts) {
1579
+ const store = ProviderStore.open(opts.dataDir);
1580
+ if (opts.alias !== void 0 && opts.alias !== store.alias) store.setAlias(opts.alias);
1581
+ const limits = new LimitEnforcer({ dataDir: opts.dataDir });
1582
+ const secretsStore = SecretsStore.open(opts.dataDir);
1583
+ const secrets = opts.secrets ?? ((name) => secretsStore.resolve(name)?.headerValue);
1584
+ const fabric = await openOrCreateFabric(opts.dataDir, opts.relayUrls);
1585
+ const engine = new ProviderEngine({
1586
+ fabric,
1587
+ store,
1588
+ dataDir: opts.dataDir,
1589
+ limits,
1590
+ opts: {
1591
+ alias: opts.alias,
1592
+ logUsage: opts.logUsage,
1593
+ timeouts: opts.timeouts,
1594
+ env: opts.env,
1595
+ secrets
1596
+ }
1597
+ });
1598
+ await engine.start();
1599
+ const endpointId = fabric.endpointId;
1600
+ const fabricIdHex = await fabric.fabricIdHex();
1601
+ const relayStatus = await fabric.relayStatus();
1602
+ const relayUrls = [...relayStatus.urls];
1603
+ const banner = composeStartupBanner({
1604
+ endpointId,
1605
+ fabricIdHex,
1606
+ dataDir: opts.dataDir,
1607
+ alias: engine.alias(),
1608
+ serviceCount: store.listServices().length,
1609
+ groupCount: store.listGroups().length,
1610
+ activeKeyCount: store.listKeys().filter((k) => k.revokedAt === void 0).length,
1611
+ revokedKeyCount: store.listKeys().filter((k) => k.revokedAt !== void 0).length,
1612
+ relayMode: relayStatus.mode,
1613
+ relayUrls,
1614
+ emptyEnvRefs: findEmptyEnvRefs(store.listServices(), opts.env ?? process.env)
1615
+ });
1616
+ let stopped = false;
1617
+ let watcher = null;
1618
+ let reloadTimer = null;
1619
+ let pollTimer = null;
1620
+ const reload = () => {
1621
+ if (stopped) return;
1622
+ engine.reloadStore().catch((err) => {
1623
+ process.stderr.write(`warn: store reload failed: ${err.message}\n`);
1624
+ }).finally(() => {
1625
+ reloadTimer = null;
1626
+ });
1627
+ };
1628
+ if (opts.watch !== false) {
1629
+ ProviderStore.filePath(opts.dataDir);
1630
+ const schedule = () => {
1631
+ if (reloadTimer !== null) return;
1632
+ reloadTimer = setTimeout(reload, 150);
1633
+ };
1634
+ try {
1635
+ watcher = watch(opts.dataDir, (_event, filename) => {
1636
+ if (filename === null || filename === "services.json" || String(filename).startsWith("services.json")) schedule();
1637
+ });
1638
+ } catch {
1639
+ watcher = null;
1640
+ }
1641
+ pollTimer = setInterval(reload, 3e4);
1642
+ pollTimer.unref?.();
1643
+ }
1644
+ const stop = async () => {
1645
+ if (stopped) return;
1646
+ stopped = true;
1647
+ if (reloadTimer !== null) clearTimeout(reloadTimer);
1648
+ if (pollTimer !== null) clearInterval(pollTimer);
1649
+ watcher?.close();
1650
+ await engine.shutdown();
1651
+ await fabric.shutdown();
1652
+ };
1653
+ return {
1654
+ engine,
1655
+ fabric,
1656
+ endpointId,
1657
+ fabricIdHex,
1658
+ relayUrls,
1659
+ relayMode: relayStatus.mode,
1660
+ banner,
1661
+ stop
1662
+ };
1663
+ }
1664
+ //#endregion
1665
+ export { detailDisplayLines as a, buildServiceEntry as i, startProviderDaemon as n, buildServiceDetail as r, openExistingFabric as t };