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,1295 @@
1
+ import { n as UsageError } from "./errors-BGtxPkM7.mjs";
2
+ import { i as randomZ32, n as resolveRelayUrls, t as loadConfig } from "./config-CO3WyW2H.mjs";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { z } from "zod";
7
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
8
+ import { oc } from "@orpc/contract";
9
+ z.enum([
10
+ "NOT_FOUND",
11
+ "CONFLICT",
12
+ "INVALID_INPUT",
13
+ "INVALID_STATE",
14
+ "UNAVAILABLE",
15
+ "INTERNAL"
16
+ ]);
17
+ /** 与错误码绑定的稳定传输状态(skill-creator-v2 同款 ErrorMap 形状)。 */
18
+ const RpcErrorDefinitions = {
19
+ NOT_FOUND: {
20
+ status: 404,
21
+ message: "The requested resource was not found."
22
+ },
23
+ CONFLICT: {
24
+ status: 409,
25
+ message: "The operation conflicts with current state."
26
+ },
27
+ INVALID_INPUT: {
28
+ status: 422,
29
+ message: "The input failed validation."
30
+ },
31
+ INVALID_STATE: {
32
+ status: 409,
33
+ message: "The operation is not valid in the current state."
34
+ },
35
+ UNAVAILABLE: {
36
+ status: 503,
37
+ message: "The required engine is not running."
38
+ },
39
+ INTERNAL: {
40
+ status: 500,
41
+ message: "Internal error."
42
+ }
43
+ };
44
+ const SERVICE_MATCH_SCHEMA = z.strictObject({
45
+ type: z.enum([
46
+ "exact",
47
+ "suffix",
48
+ "regex"
49
+ ]),
50
+ value: z.string().min(1).max(2048)
51
+ });
52
+ const SERVICE_REWRITE_SCHEMA = z.strictObject({
53
+ hostHeader: z.string().min(1).max(2048).optional(),
54
+ pathPrefixStrip: z.string().min(1).max(2048).optional(),
55
+ pathPrefixAppend: z.string().min(1).max(2048).optional(),
56
+ headerSet: z.record(z.string().min(1).max(1024), z.string().max(8192)).optional(),
57
+ headerRemove: z.array(z.string().min(1).max(1024)).max(32).optional()
58
+ });
59
+ const ROUTE_FORM_SCHEMA = z.enum([
60
+ "openai-chat",
61
+ "openai-responses",
62
+ "anthropic"
63
+ ]);
64
+ /** 路由匹配模式(M3-r7):prefix = 前缀替换(默认);pattern = URLPattern
65
+ * 匹配 + RFC 6570 URI Template 拼装。 */
66
+ const ROUTE_MODE_SCHEMA = z.enum(["prefix", "pattern"]);
67
+ /**
68
+ * 路径路由(M3-r7 定形):**按声明顺序命中**(先声明先匹配)。
69
+ * - prefix 模式(默认):localPrefix(版本段粒度如 /v1)替换为 upstreamPrefix;
70
+ * - pattern 模式:matchPattern(URLPattern pathname 表达式,`:name`/`{name}`
71
+ * 组、`*` 通配)匹配请求路径,template(RFC 6570 URI Template)以捕获组
72
+ * + 查询参数拼装 upstream 路径。
73
+ * forms 标注承载的 API 标准(AI 层;引擎转发与 forms 无关,可为空)。
74
+ */
75
+ const SERVICE_ROUTE_SCHEMA = z.strictObject({
76
+ forms: z.array(ROUTE_FORM_SCHEMA).max(3),
77
+ mode: ROUTE_MODE_SCHEMA.optional(),
78
+ /** prefix 模式:本地端口前缀。缺省 = 首个 form 的规范前缀。 */
79
+ localPrefix: z.string().min(1).max(2048).optional(),
80
+ /** prefix 模式:替换本地前缀的 upstream 路径前缀("" = upstream 根)。 */
81
+ upstreamPrefix: z.string().max(2048).optional(),
82
+ /** pattern 模式:URLPattern pathname 表达式。 */
83
+ matchPattern: z.string().min(1).max(2048).optional(),
84
+ /** pattern 模式:RFC 6570 URI Template(变量 = 捕获组 + 查询参数)。 */
85
+ template: z.string().min(1).max(2048).optional()
86
+ });
87
+ /** 各 API 标准在本地端口上的规范前缀(缺省 localPrefix 与 agent 配置基准)。 */
88
+ const ROUTE_LOCAL_PREFIX = {
89
+ "openai-chat": "/v1",
90
+ "openai-responses": "/v1",
91
+ anthropic: "/anthropic"
92
+ };
93
+ /** 路由的生效本地前缀(缺省派生规范前缀)。 */
94
+ function routeLocalPrefix(route) {
95
+ return route.localPrefix ?? ROUTE_LOCAL_PREFIX[route.forms[0] ?? "openai-chat"];
96
+ }
97
+ const SERVICE_SCHEMA = z.strictObject({
98
+ serviceId: z.string().min(1).max(128),
99
+ name: z.string().min(1).max(256),
100
+ match: z.array(SERVICE_MATCH_SCHEMA).max(64),
101
+ upstream: z.string().min(1).max(2048),
102
+ rewrite: SERVICE_REWRITE_SCHEMA.optional(),
103
+ routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional(),
104
+ defaultPort: z.number().int().min(1).max(65535)
105
+ });
106
+ /** services.add 输入(defaultPort 缺省规则的校验在 store,同 CLI)。 */
107
+ const SERVICE_INPUT_SCHEMA = z.strictObject({
108
+ name: z.string().min(1).max(256),
109
+ upstream: z.string().min(1).max(2048),
110
+ match: z.array(SERVICE_MATCH_SCHEMA).min(1).max(64),
111
+ defaultPort: z.number().int().min(1).max(65535).optional(),
112
+ rewrite: SERVICE_REWRITE_SCHEMA.optional(),
113
+ routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional()
114
+ });
115
+ const GROUP_LIMITS_SCHEMA = z.strictObject({
116
+ maxConcurrency: z.number().int().min(1).optional(),
117
+ dailyRequests: z.number().int().min(1).optional()
118
+ });
119
+ const GROUP_SCHEMA = z.strictObject({
120
+ name: z.string().min(1).max(256),
121
+ serviceIds: z.array(z.string().min(1).max(128)).max(256),
122
+ limits: GROUP_LIMITS_SCHEMA.optional()
123
+ });
124
+ /** 密钥视图:keyId/分组/时间/状态;哈希与原文一律不进契约(原文仅 issue 时一次性返回)。 */
125
+ const KEY_VIEW_SCHEMA = z.strictObject({
126
+ keyId: z.string().min(1).max(128),
127
+ group: z.string().min(1).max(256),
128
+ createdAt: z.number().int().min(0),
129
+ revokedAt: z.number().int().min(0).optional()
130
+ });
131
+ /** 使用方服务目录条目(AUTH_OK / import 视图;detail 为脱敏披露,$env 值显示 ●)。 */
132
+ const SERVICE_ENTRY_SCHEMA = z.object({
133
+ serviceId: z.string(),
134
+ name: z.string(),
135
+ match: z.array(z.strictObject({
136
+ type: z.string(),
137
+ value: z.string()
138
+ })),
139
+ defaultPort: z.number().int(),
140
+ detail: z.object({
141
+ upstream: z.string(),
142
+ match: z.array(z.strictObject({
143
+ type: z.string(),
144
+ value: z.string()
145
+ })),
146
+ rewrite: z.object({
147
+ host: z.string().optional(),
148
+ prefix: z.string().optional(),
149
+ headerSet: z.array(z.strictObject({
150
+ name: z.string(),
151
+ value: z.string()
152
+ })).optional()
153
+ }).optional(),
154
+ /** 按标准路由披露(消费侧呈现各标准本地 base 与可用性判定)。 */
155
+ routes: z.array(z.strictObject({
156
+ forms: z.array(ROUTE_FORM_SCHEMA).max(3),
157
+ mode: ROUTE_MODE_SCHEMA.optional(),
158
+ localPrefix: z.string().optional(),
159
+ upstreamPrefix: z.string().optional(),
160
+ matchPattern: z.string().optional(),
161
+ template: z.string().optional()
162
+ })).max(4).optional()
163
+ }).optional()
164
+ });
165
+ /** 消费侧提供者连接状态(M1 六态 + 网关未运行时的 stopped)。 */
166
+ const PROVIDER_STATE_SCHEMA = z.enum([
167
+ "stopped",
168
+ "not-connected",
169
+ "connected-unauthed",
170
+ "direct",
171
+ "relay",
172
+ "offline",
173
+ "key-all-invalid"
174
+ ]);
175
+ const CONSUMER_PROVIDER_STATUS_SCHEMA = z.object({
176
+ endpointId: z.string(),
177
+ alias: z.string(),
178
+ state: PROVIDER_STATE_SCHEMA,
179
+ /** 该提供者授权目录内的服务(脱敏 detail)。 */
180
+ services: z.array(SERVICE_ENTRY_SCHEMA),
181
+ /** serviceId -> 本地端口(pinned 或 default;网关运行时为实际监听端口)。 */
182
+ ports: z.record(z.string(), z.number().int().min(0).max(65535)),
183
+ servedCount: z.number().int().min(0),
184
+ bufferOverflows: z.number().int().min(0),
185
+ lastError: z.string().optional()
186
+ });
187
+ const API_FORM_SCHEMA = z.enum([
188
+ "openai-completions",
189
+ "anthropic-messages",
190
+ "gemini-native"
191
+ ]);
192
+ const PRESET_SCHEMA = z.strictObject({
193
+ id: z.string().min(1).max(128),
194
+ label: z.string().min(1).max(256),
195
+ apiForm: API_FORM_SCHEMA,
196
+ baseUrl: z.string().min(1).max(2048),
197
+ /** 图标源 id(models.dev logos 覆写;缺省取 id)。 */
198
+ iconId: z.string().min(1).max(128).optional(),
199
+ /** 惯用环境变量名(仅用于 $env 注入建议与文档;本地运行时模板无此字段)。 */
200
+ keyEnv: z.string().min(1).max(256).optional(),
201
+ /** 使用方本地端口建议(避开 <1024 特权段)。 */
202
+ defaultPort: z.number().int().min(1024).max(65535),
203
+ /** 官方域名集(exact/suffix 建议的生成源)。 */
204
+ matchDomains: z.array(z.string().min(1).max(256)).min(1).max(16),
205
+ /** 按标准路由(展开进服务;本地前缀由 form 派生)。 */
206
+ routes: z.array(SERVICE_ROUTE_SCHEMA).max(3).optional(),
207
+ notes: z.string().max(2048).optional(),
208
+ /** 出处(精选集为调研 URL;models.dev 长尾为 "models.dev")。 */
209
+ source: z.string().min(1).max(512),
210
+ /** apiForm 未经厂商文档核实(models.dev npm 推断)时长尾条目置 true。 */
211
+ unverified: z.boolean().optional()
212
+ });
213
+ const SECRET_NAME_SCHEMA = z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._-]*$/, "lowercase letters, digits, dot, dash, underscore");
214
+ /** 密钥库清单条目(仅名称与开关——值由设计不跨 RPC)。 */
215
+ const SECRET_ENTRY_SCHEMA = z.strictObject({
216
+ name: SECRET_NAME_SCHEMA,
217
+ createdAt: z.number().int().min(0),
218
+ updatedAt: z.number().int().min(0),
219
+ /** 注入时自动拼 "Bearer "(默认 true;Owner 2026-09-10:值默认是裸 key)。 */
220
+ bearerPrefix: z.boolean()
221
+ });
222
+ const WRITER_AGENT_SCHEMA = z.enum([
223
+ "codex",
224
+ "claude-code",
225
+ "cursor",
226
+ "cline",
227
+ "continue"
228
+ ]);
229
+ /** 写手目标描述:serviceId(从消费方存储解析端口)或显式 port,二选一。 */
230
+ const WRITER_TARGET_SCHEMA = z.strictObject({
231
+ serviceId: z.string().min(1).max(128).optional(),
232
+ port: z.number().int().min(1).max(65535).optional()
233
+ }).refine((v) => v.serviceId !== void 0 !== (v.port !== void 0), { message: "exactly one of serviceId or port is required" });
234
+ const WRITER_PREVIEW_SCHEMA = z.strictObject({
235
+ agent: WRITER_AGENT_SCHEMA,
236
+ target: WRITER_TARGET_SCHEMA
237
+ });
238
+ const WRITER_APPLY_SCHEMA = z.strictObject({
239
+ agent: WRITER_AGENT_SCHEMA,
240
+ target: WRITER_TARGET_SCHEMA,
241
+ /** preview 返回的确认令牌(sha256(diff)):apply 只写入用户看过的那份 diff。 */
242
+ confirmToken: z.string().regex(/^[0-9a-f]{64}$/)
243
+ });
244
+ /** 按标准测试的结果(消费侧 wire 链路 / 提供方 route 直打共用形状)。 */
245
+ const SERVICE_TEST_RESULT_SCHEMA = z.strictObject({
246
+ ok: z.boolean(),
247
+ latencyMs: z.number().int().min(0),
248
+ request: z.strictObject({
249
+ method: z.literal("POST"),
250
+ url: z.string(),
251
+ model: z.string().optional()
252
+ }),
253
+ modelSource: z.enum([
254
+ "explicit",
255
+ "models.dev",
256
+ "none"
257
+ ]).optional(),
258
+ /** upstream 响应状态码(拿到响应即有;传输失败缺席)。 */
259
+ httpStatus: z.number().int().min(100).max(599).optional(),
260
+ error: z.string().optional(),
261
+ /** 响应正文摘录(成功=模型回复/错误体;截断)。 */
262
+ bodyExcerpt: z.string().optional()
263
+ });
264
+ const SETTINGS_SCHEMA = z.strictObject({
265
+ theme: z.enum([
266
+ "dark",
267
+ "light",
268
+ "system"
269
+ ]),
270
+ /** false 时预设列表不含 models.dev 长尾(断网/隐私偏好)。 */
271
+ modelsDevEnabled: z.boolean(),
272
+ /** relay 入口列表(provider/consumer 共用;null = 未配置,走 SDK 默认)。 */
273
+ relayUrls: z.array(z.string().min(1).max(2048)).max(8).nullable()
274
+ });
275
+ const SHARE_CREATE_INPUT_SCHEMA = z.strictObject({
276
+ group: z.string().min(1).max(256),
277
+ /** TTL 毫秒(1s..30d;缺省 60min——范围校验复用 link.ts 常量语义)。 */
278
+ ttlMs: z.number().int().min(1e3).max(2592e6).optional()
279
+ });
280
+ const PRESET_APPLY_INPUT_SCHEMA = z.strictObject({
281
+ presetId: z.string().min(1).max(128),
282
+ /** 服务名(缺省取 preset.id)。 */
283
+ name: z.string().min(1).max(256).optional(),
284
+ /** 显式端口(缺省取 preset.defaultPort;上游特权端口时必填——store 校验兜底)。 */
285
+ port: z.number().int().min(1).max(65535).optional(),
286
+ /** 密钥库名(选中时 rewrite 写入 $secret:<name>,优先于 keyEnv)。 */
287
+ secretName: SECRET_NAME_SCHEMA.optional(),
288
+ /** 注入建议的 $env 变量名(缺省取 preset.keyEnv;无 keyEnv 且未显式给出则不注入)。 */
289
+ keyEnv: z.string().min(1).max(256).optional()
290
+ });
291
+ /** 提供方状态快照(引擎运行值 + 磁盘存储摘要,二合一)。 */
292
+ const PROVIDER_STATUS_SCHEMA = z.object({
293
+ running: z.boolean(),
294
+ alias: z.string().optional(),
295
+ endpointId: z.string().optional(),
296
+ fabricIdHex: z.string().optional(),
297
+ relayMode: z.string().optional(),
298
+ relayUrls: z.array(z.string()),
299
+ sessionCount: z.number().int().min(0),
300
+ services: z.number().int().min(0),
301
+ groups: z.number().int().min(0),
302
+ activeKeys: z.number().int().min(0),
303
+ revokedKeys: z.number().int().min(0)
304
+ });
305
+ oc.errors(RpcErrorDefinitions).router({
306
+ provider: {
307
+ services: {
308
+ /** 全量服务配置(本机控制面:含 rewrite 原始 $env 引用,无脱敏需要)。 */
309
+ list: oc.input(z.object({})).output(z.object({ services: z.array(SERVICE_SCHEMA) })),
310
+ /** 按 name 精确定位单个服务。 */
311
+ get: oc.input(z.strictObject({ name: z.string().min(1).max(256) })).output(SERVICE_SCHEMA),
312
+ /** 新增服务(校验同 CLI:defaultPort 特权规则、match 编译检查、重名拒绝)。 */
313
+ add: oc.input(SERVICE_INPUT_SCHEMA).output(z.object({ service: SERVICE_SCHEMA })),
314
+ /** 删除服务(连带清出分组引用)。 */
315
+ remove: oc.input(z.strictObject({ name: z.string().min(1).max(256) })).output(z.object({ removed: z.literal(true) })),
316
+ /** 上游连通性测试(草稿或已存服务形状;provider-local、不落盘、不计限额)。 */
317
+ test: oc.input(z.strictObject({
318
+ upstream: z.string().min(1).max(2048),
319
+ apiForm: API_FORM_SCHEMA.optional(),
320
+ secretName: SECRET_NAME_SCHEMA.optional(),
321
+ model: z.string().min(1).max(256).optional()
322
+ })).output(z.strictObject({
323
+ ok: z.boolean(),
324
+ httpStatus: z.number().int().min(0).max(599).optional(),
325
+ latencyMs: z.number().int().min(0),
326
+ model: z.string(),
327
+ error: z.string().optional(),
328
+ /** 请求详情(发起过即有):UI 呈现「发了什么」。 */
329
+ request: z.strictObject({
330
+ method: z.literal("POST"),
331
+ url: z.string(),
332
+ model: z.string()
333
+ }).optional(),
334
+ /** 模型选择来源(explicit / models.dev / upstream-probe)。 */
335
+ modelSource: z.enum([
336
+ "explicit",
337
+ "models.dev",
338
+ "upstream-probe"
339
+ ]).optional()
340
+ })),
341
+ /**
342
+ * 提供方侧按标准路由测试(Owner 裁决 2026-09-11:与 connect ③ 同形态):
343
+ * 最小 AI-API 请求 → 本地路由命中(prefix/pattern + 白名单)→ rewrite
344
+ * 注入($secret/$env)→ 直打 upstream。request.url = 改写后的上游 URL。
345
+ */
346
+ testRoute: oc.input(z.strictObject({
347
+ name: z.string().min(1).max(256),
348
+ form: ROUTE_FORM_SCHEMA,
349
+ model: z.string().min(1).max(256).optional(),
350
+ content: z.string().max(8192).optional(),
351
+ localPrefix: z.string().min(1).max(2048).optional()
352
+ })).output(SERVICE_TEST_RESULT_SCHEMA)
353
+ },
354
+ secrets: {
355
+ /** 密钥库清单(仅名称与时间戳;值由设计不跨 RPC)。 */
356
+ list: oc.input(z.object({})).output(z.object({ secrets: z.array(SECRET_ENTRY_SCHEMA) })),
357
+ /** 新增/覆写(value 为裸密钥——bearerPrefix 默认 true 时注入自动拼 "Bearer ")。 */
358
+ set: oc.input(z.strictObject({
359
+ name: SECRET_NAME_SCHEMA,
360
+ value: z.string().min(1).max(8192),
361
+ /** 关闭后按原样注入(非 Bearer 站点)。 */
362
+ bearerPrefix: z.boolean().optional()
363
+ })).output(z.object({ secret: SECRET_ENTRY_SCHEMA })),
364
+ /** 删除(不存在报 NOT_FOUND)。 */
365
+ remove: oc.input(z.strictObject({ name: SECRET_NAME_SCHEMA })).output(z.object({ removed: z.literal(true) }))
366
+ },
367
+ groups: {
368
+ /** 分组列表(含 limits 与 serviceIds)。 */
369
+ list: oc.input(z.object({})).output(z.object({ groups: z.array(GROUP_SCHEMA) })),
370
+ /** 新建分组(可带限额;未知服务名报错——store 校验)。 */
371
+ add: oc.input(z.strictObject({
372
+ name: z.string().min(1).max(256),
373
+ serviceNames: z.array(z.string().min(1).max(256)).max(256),
374
+ limits: GROUP_LIMITS_SCHEMA.optional()
375
+ })).output(z.object({ group: GROUP_SCHEMA })),
376
+ /** 整表替换分组服务引用。 */
377
+ setServices: oc.input(z.strictObject({
378
+ name: z.string().min(1).max(256),
379
+ serviceNames: z.array(z.string().min(1).max(256)).max(256)
380
+ })).output(z.object({ group: GROUP_SCHEMA })),
381
+ /** 更新分组限额(省略 limits = 清除为无限)。 */
382
+ setLimits: oc.input(z.strictObject({
383
+ name: z.string().min(1).max(256),
384
+ limits: GROUP_LIMITS_SCHEMA.optional()
385
+ })).output(z.object({ group: GROUP_SCHEMA })),
386
+ /** 删除分组(仍有未撤销密钥时 CONFLICT——先 revoke)。 */
387
+ remove: oc.input(z.strictObject({ name: z.string().min(1).max(256) })).output(z.object({ removed: z.literal(true) }))
388
+ },
389
+ keys: {
390
+ /** 签发(原文仅本次返回;此后只余哈希)。 */
391
+ issue: oc.input(z.strictObject({ group: z.string().min(1).max(256) })).output(z.object({
392
+ keyId: z.string(),
393
+ key: z.string(),
394
+ createdAt: z.number().int()
395
+ })),
396
+ /** 密钥清单(无哈希、无原文)。 */
397
+ list: oc.input(z.object({})).output(z.object({ keys: z.array(KEY_VIEW_SCHEMA) })),
398
+ /** 撤销(幂等)。 */
399
+ revoke: oc.input(z.strictObject({ keyId: z.string().min(1).max(128) })).output(z.object({ key: KEY_VIEW_SCHEMA }))
400
+ },
401
+ share: {
402
+ /** 组合分享链接(需要 provider daemon 在运行以签发 fabric invite)。 */
403
+ create: oc.input(SHARE_CREATE_INPUT_SCHEMA).output(z.object({
404
+ link: z.string().min(1),
405
+ keyId: z.string(),
406
+ warnings: z.array(z.string())
407
+ })) },
408
+ /** 提供方状态(引擎运行值 + 磁盘摘要)。 */
409
+ status: oc.input(z.object({})).output(PROVIDER_STATUS_SCHEMA),
410
+ daemon: {
411
+ /** 启动提供方 daemon(幂等;按数据目录现状装配)。 */
412
+ start: oc.input(z.object({})).output(z.object({ running: z.literal(true) })),
413
+ /** 停止提供方 daemon(幂等;在途请求按 M1 语义收敛)。 */
414
+ stop: oc.input(z.object({})).output(z.object({ running: z.literal(false) }))
415
+ }
416
+ },
417
+ consumer: {
418
+ import: {
419
+ /** 分享链接离线预览(不发起任何网络请求)。 */
420
+ preview: oc.input(z.strictObject({ link: z.string().min(1) })).output(z.object({
421
+ alias: z.string(),
422
+ endpointId: z.string(),
423
+ group: z.string(),
424
+ keyId: z.string(),
425
+ relayUrls: z.array(z.string()),
426
+ services: z.array(z.object({
427
+ serviceId: z.string(),
428
+ name: z.string(),
429
+ defaultPort: z.number().int(),
430
+ matchCount: z.number().int().min(0)
431
+ }))
432
+ })),
433
+ /** 兑换令牌并入环(老设备复用既有身份,不消耗令牌)。 */
434
+ apply: oc.input(z.strictObject({ link: z.string().min(1) })).output(z.object({
435
+ alias: z.string(),
436
+ endpointId: z.string(),
437
+ redeemed: z.boolean(),
438
+ keyAdded: z.boolean(),
439
+ services: z.array(z.object({
440
+ serviceId: z.string(),
441
+ name: z.string(),
442
+ defaultPort: z.number().int()
443
+ }))
444
+ }))
445
+ },
446
+ /** fabric 邀请令牌入网(裸 join,不带密钥)。 */
447
+ join: oc.input(z.strictObject({ invite: z.string().min(1) })).output(z.object({
448
+ alias: z.string(),
449
+ endpointId: z.string(),
450
+ alreadyJoined: z.boolean()
451
+ })),
452
+ key: {
453
+ /** 裸密钥入环(keyId/group 由下次 AUTH_OK 回填)。 */
454
+ add: oc.input(z.strictObject({
455
+ key: z.string().min(8).max(256),
456
+ providerRef: z.string().min(1).max(256)
457
+ })).output(z.object({
458
+ alias: z.string(),
459
+ added: z.boolean()
460
+ })) },
461
+ services: {
462
+ /**
463
+ * 消费侧连通测试(M3-r8:③ 步 = test——选协议、选端点、单轮输入框发
464
+ * 真实 AI 请求):对本机网关端口按 API 标准发最小请求,走完整 wire 链路;
465
+ * 凭据由提供方 rewrite 注入,本请求不携带 authorization。
466
+ * model 缺省时经 models.dev 缓存按 detail.upstream 选最便宜 chat 模型;
467
+ * content 为单轮提示词(缺省 "ping");localPrefix 显式指定端点路径
468
+ * (缺省取该标准路由规则的本地前缀)。
469
+ */
470
+ test: oc.input(z.strictObject({
471
+ serviceId: z.string().min(1).max(128),
472
+ form: ROUTE_FORM_SCHEMA,
473
+ model: z.string().min(1).max(256).optional(),
474
+ content: z.string().max(8192).optional(),
475
+ localPrefix: z.string().min(1).max(2048).optional()
476
+ })).output(SERVICE_TEST_RESULT_SCHEMA) },
477
+ ports: {
478
+ /** 全部已导入服务的本地端口清单(pinned/default 标注)。 */
479
+ list: oc.input(z.object({})).output(z.object({ providers: z.array(z.object({
480
+ alias: z.string(),
481
+ endpointId: z.string(),
482
+ services: z.array(z.object({
483
+ serviceId: z.string(),
484
+ name: z.string(),
485
+ port: z.number().int(),
486
+ defaultPort: z.number().int(),
487
+ pinned: z.boolean()
488
+ }))
489
+ })) })),
490
+ /** 持久化端口偏好(运行中网关下次启动生效)。 */
491
+ set: oc.input(z.strictObject({
492
+ serviceId: z.string().min(1).max(128),
493
+ port: z.number().int().min(1).max(65535)
494
+ })).output(z.object({
495
+ alias: z.string(),
496
+ serviceId: z.string(),
497
+ port: z.number().int()
498
+ }))
499
+ },
500
+ /** 消费侧状态(网关运行时为实时状态机;停止时为 stopped + 存储投影)。 */
501
+ status: oc.input(z.object({})).output(z.object({
502
+ gatewayRunning: z.boolean(),
503
+ providers: z.array(CONSUMER_PROVIDER_STATUS_SCHEMA)
504
+ })),
505
+ /** 整环删除(钥环 + fabric 身份目录)。 */
506
+ forget: oc.input(z.strictObject({ ref: z.string().min(1).max(256) })).output(z.object({
507
+ alias: z.string(),
508
+ removed: z.literal(true)
509
+ })),
510
+ gateway: {
511
+ /** 启动本地网关(幂等;物化既有目录监听)。 */
512
+ start: oc.input(z.object({})).output(z.object({ running: z.literal(true) })),
513
+ /** 停止本地网关(幂等)。 */
514
+ stop: oc.input(z.object({})).output(z.object({ running: z.literal(false) }))
515
+ }
516
+ },
517
+ presets: {
518
+ /** 预设列表(精选 + 可选 models.dev 长尾合流)。 */
519
+ list: oc.input(z.strictObject({ includeModelsDev: z.boolean().optional() })).output(z.object({
520
+ curated: z.array(PRESET_SCHEMA),
521
+ modelsDev: z.array(PRESET_SCHEMA),
522
+ /** 长尾不可用原因(断网/禁用/无缓存;精选集仍可用)。 */
523
+ modelsDevError: z.string().optional()
524
+ })),
525
+ /** 预设 → 提供方服务(展开 upstream/match/defaultPort/$env 注入;走 services.add 同一校验)。 */
526
+ applyAsService: oc.input(PRESET_APPLY_INPUT_SCHEMA).output(z.object({
527
+ service: SERVICE_SCHEMA,
528
+ /** $env 注入建议(导出提示;值需含完整 header 形态如 "Bearer <key>")。 */
529
+ envHint: z.string().optional()
530
+ })),
531
+ /** 模型清单(models.dev 缓存;按价格升序,chat 优先,未知价尾排)。
532
+ * presetId 二选一:已知预设;或 custom(自定义上游)实时探测 {upstream}/models。 */
533
+ models: oc.input(z.strictObject({
534
+ presetId: z.string().min(1).max(128).optional(),
535
+ upstream: z.string().min(1).max(2048).optional(),
536
+ secretName: SECRET_NAME_SCHEMA.optional()
537
+ }).refine((v) => v.presetId !== void 0 !== (v.upstream !== void 0), { message: "exactly one of presetId or upstream is required" })).output(z.strictObject({
538
+ models: z.array(z.strictObject({
539
+ id: z.string().min(1).max(256),
540
+ name: z.string().max(512).optional(),
541
+ /** input+output 合计 USD/Mtok;未知价省略。 */
542
+ pricePerMTok: z.number().min(0).optional(),
543
+ /** 价格已知(排序依据;未知价条目 false)。 */
544
+ priced: z.boolean(),
545
+ /** false = embed/image/tts 等非对话模型(id 启发式)。 */
546
+ chat: z.boolean()
547
+ })),
548
+ /** 清单不可用原因(无缓存且拉取失败等)。 */
549
+ error: z.string().optional()
550
+ }))
551
+ },
552
+ writers: {
553
+ /** 生成目标 agent 配置的统一 diff 与落盘路径(不写盘)。 */
554
+ preview: oc.input(WRITER_PREVIEW_SCHEMA).output(z.object({
555
+ agent: WRITER_AGENT_SCHEMA,
556
+ path: z.string().min(1),
557
+ /** 目标文件当前是否已存在(新建 vs 更新)。 */
558
+ exists: z.boolean(),
559
+ /** 解析后的本地端点(http://127.0.0.1:<port>)。 */
560
+ baseUrl: z.string(),
561
+ /** 统一 diff 文本(无上下文行数的完整 unified diff)。 */
562
+ diff: z.string(),
563
+ /** apply 必须原样带回(sha256(diff))。 */
564
+ confirmToken: z.string().regex(/^[0-9a-f]{64}$/)
565
+ })),
566
+ /** 确认后原子写(confirmToken 不匹配即拒绝;保留其余配置字段)。 */
567
+ apply: oc.input(WRITER_APPLY_SCHEMA).output(z.object({
568
+ agent: WRITER_AGENT_SCHEMA,
569
+ path: z.string(),
570
+ written: z.literal(true)
571
+ }))
572
+ },
573
+ system: {
574
+ settings: {
575
+ /** 读取应用设置(主题/models.dev 开关/relay)。 */
576
+ get: oc.input(z.object({})).output(SETTINGS_SCHEMA),
577
+ /** 补丁式更新(仅提交的字段变更)。 */
578
+ set: oc.input(z.strictObject({
579
+ theme: z.enum([
580
+ "dark",
581
+ "light",
582
+ "system"
583
+ ]).optional(),
584
+ modelsDevEnabled: z.boolean().optional(),
585
+ relayUrls: z.array(z.string().min(1).max(2048)).max(8).nullable().optional()
586
+ })).output(SETTINGS_SCHEMA)
587
+ },
588
+ /** 通知通道常量(前端 ws 订阅地址;与 web-server 实现保持同源)。 */
589
+ notifyChannels: oc.input(z.object({})).output(z.object({
590
+ rpcPath: z.literal("/ws/rpc"),
591
+ notifyPath: z.literal("/ws/notify")
592
+ }))
593
+ }
594
+ });
595
+ //#endregion
596
+ //#region src/provider/match-pattern.ts
597
+ var MatchPatternError = class extends Error {
598
+ constructor(message) {
599
+ super(message);
600
+ this.name = "MatchPatternError";
601
+ }
602
+ };
603
+ /** 花括号组 → 冒号组(兼容两种 URLPattern 文档写法)。 */
604
+ function normalizeMatchPatternSyntax(raw) {
605
+ return raw.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, ":$1");
606
+ }
607
+ const compiled = /* @__PURE__ */ new Map();
608
+ function compileMatchPattern(raw) {
609
+ const cached = compiled.get(raw);
610
+ if (cached !== void 0) return cached;
611
+ if (typeof URLPattern !== "function") throw new MatchPatternError("URLPattern is unavailable in this runtime");
612
+ const translated = normalizeMatchPatternSyntax(raw);
613
+ let pattern;
614
+ try {
615
+ pattern = new URLPattern({ pathname: translated });
616
+ } catch (err) {
617
+ throw new MatchPatternError(err.message || "invalid urlpattern");
618
+ }
619
+ compiled.set(raw, pattern);
620
+ return pattern;
621
+ }
622
+ /**
623
+ * 匹配请求路径(含可选查询串)。返回捕获组(pathname groups);未命中返回
624
+ * null。exec 需要完整 URL——以 http://localhost 为基座构造。
625
+ */
626
+ function matchRequestPath(pattern, pathname, search) {
627
+ const url = `http://localhost${pathname.startsWith("/") ? pathname : `/${pathname}`}${search === "" ? "" : `?${search}`}`;
628
+ const result = pattern.exec(url);
629
+ if (result === null) return null;
630
+ return { ...result.pathname.groups };
631
+ }
632
+ //#endregion
633
+ //#region src/provider/uri-template.ts
634
+ var UriTemplateError = class extends Error {
635
+ constructor(message) {
636
+ super(message);
637
+ this.name = "UriTemplateError";
638
+ }
639
+ };
640
+ const OPERATORS = {
641
+ "": {
642
+ first: "",
643
+ sep: ",",
644
+ named: false,
645
+ ifemp: "",
646
+ allowReserved: false
647
+ },
648
+ "+": {
649
+ first: "",
650
+ sep: ",",
651
+ named: false,
652
+ ifemp: "",
653
+ allowReserved: true
654
+ },
655
+ ".": {
656
+ first: ".",
657
+ sep: ".",
658
+ named: false,
659
+ ifemp: "",
660
+ allowReserved: false
661
+ },
662
+ "/": {
663
+ first: "/",
664
+ sep: "/",
665
+ named: false,
666
+ ifemp: "",
667
+ allowReserved: false
668
+ },
669
+ ";": {
670
+ first: ";",
671
+ sep: ";",
672
+ named: true,
673
+ ifemp: "",
674
+ allowReserved: false
675
+ },
676
+ "?": {
677
+ first: "?",
678
+ sep: "&",
679
+ named: true,
680
+ ifemp: "=",
681
+ allowReserved: false
682
+ },
683
+ "&": {
684
+ first: "&",
685
+ sep: "&",
686
+ named: true,
687
+ ifemp: "=",
688
+ allowReserved: false
689
+ },
690
+ "#": {
691
+ first: "#",
692
+ sep: ",",
693
+ named: false,
694
+ ifemp: "",
695
+ allowReserved: true
696
+ }
697
+ };
698
+ const UNRESERVED = /[A-Za-z0-9\-._~]/;
699
+ const RESERVED = /[A-Za-z0-9\-._~:\/\?#\[\]@!\$&'()*+,;=]/;
700
+ function encodeValue(value, allowReserved) {
701
+ let out = "";
702
+ for (const ch of value) if (UNRESERVED.test(ch) || allowReserved && RESERVED.test(ch)) out += ch;
703
+ else for (const byte of new TextEncoder().encode(ch)) out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
704
+ return out;
705
+ }
706
+ function parseExpression(expr) {
707
+ const opKey = /^[.+\/;?&#]/.test(expr) ? expr[0] : "";
708
+ const body = expr.slice(opKey.length);
709
+ const spec = OPERATORS[opKey];
710
+ const vars = [];
711
+ for (const raw of body.split(",")) {
712
+ const name = raw.replace(/:\d+$/, "").replace(/\*$/, "").trim();
713
+ if (name === "" || !/^[A-Za-z0-9_.%]+$/.test(name)) throw new UriTemplateError(`invalid variable name '${raw}' in expression '{${expr}}'`);
714
+ vars.push({ name });
715
+ }
716
+ if (vars.length === 0) throw new UriTemplateError(`empty expression '{${expr}}'`);
717
+ return {
718
+ op: spec,
719
+ vars
720
+ };
721
+ }
722
+ /** 校验模板(写入期 fail-fast 用):花括号配对 + 全部表达式可解析。 */
723
+ function validateUriTemplate(template) {
724
+ expandUriTemplate(template, {});
725
+ }
726
+ /** RFC 6570 子集扩展。 */
727
+ function expandUriTemplate(template, vars) {
728
+ let out = "";
729
+ let i = 0;
730
+ while (i < template.length) {
731
+ const open = template.indexOf("{", i);
732
+ if (open < 0) {
733
+ out += template.slice(i);
734
+ break;
735
+ }
736
+ out += template.slice(i, open);
737
+ const close = template.indexOf("}", open);
738
+ if (close < 0) throw new UriTemplateError(`unbalanced '{' in template '${template}'`);
739
+ const { op, vars: specs } = parseExpression(template.slice(open + 1, close));
740
+ const parts = [];
741
+ let used = false;
742
+ for (const { name } of specs) {
743
+ const raw = vars[name];
744
+ if (raw === void 0 || raw === "") continue;
745
+ used = true;
746
+ const value = encodeValue(raw, op.allowReserved);
747
+ parts.push(op.named ? `${name}=${value}` : value);
748
+ }
749
+ if (used) out += op.first + parts.join(op.sep);
750
+ i = close + 1;
751
+ }
752
+ return out;
753
+ }
754
+ //#endregion
755
+ //#region src/provider/store.ts
756
+ const DIR_MODE = 448;
757
+ const FILE_MODE = 384;
758
+ /** 密钥哈希固定 salt(所有提供方实例一致;防直接彩虹表对照 sk-aifly- 空间)。 */
759
+ const KEY_HASH_SALT = "aifly-provider-key-v1:";
760
+ const KEY_MATERIAL_PREFIX = "sk-aifly-";
761
+ /** 存储层错误(用户面 message 为英文 ASCII,直接透出 CLI)。 */
762
+ var StoreError = class extends Error {
763
+ code;
764
+ constructor(code, message) {
765
+ super(message);
766
+ this.name = "StoreError";
767
+ this.code = code;
768
+ }
769
+ };
770
+ const SERVICE_MATCH_STORE_SCHEMA = z.strictObject({
771
+ type: z.enum([
772
+ "exact",
773
+ "suffix",
774
+ "regex"
775
+ ]),
776
+ value: z.string().min(1).max(2048)
777
+ });
778
+ const SERVICE_REWRITE_STORE_SCHEMA = z.strictObject({
779
+ hostHeader: z.string().min(1).max(2048).optional(),
780
+ pathPrefixStrip: z.string().min(1).max(2048).optional(),
781
+ pathPrefixAppend: z.string().min(1).max(2048).optional(),
782
+ headerSet: z.record(z.string().min(1).max(1024), z.string().max(8192)).optional(),
783
+ headerRemove: z.array(z.string().min(1).max(1024)).max(32).optional()
784
+ });
785
+ /** 路径路由(M3-r7):按声明顺序命中的转发规则——prefix 模式(默认,
786
+ * localPrefix → upstreamPrefix)或 pattern 模式(matchPattern URLPattern
787
+ * + template RFC 6570)。forms 为 AI 层标注(可为空)。 */
788
+ const SERVICE_ROUTE_STORE_SCHEMA = z.strictObject({
789
+ forms: z.array(z.enum([
790
+ "openai-chat",
791
+ "openai-responses",
792
+ "anthropic"
793
+ ])).max(3),
794
+ mode: z.enum(["prefix", "pattern"]).optional(),
795
+ localPrefix: z.string().min(1).max(2048).optional(),
796
+ upstreamPrefix: z.string().max(2048).optional(),
797
+ matchPattern: z.string().min(1).max(2048).optional(),
798
+ template: z.string().min(1).max(2048).optional()
799
+ });
800
+ const SERVICE_STORE_SCHEMA = z.strictObject({
801
+ serviceId: z.string().min(1).max(128),
802
+ name: z.string().min(1).max(256),
803
+ match: z.array(SERVICE_MATCH_STORE_SCHEMA).max(64),
804
+ upstream: z.string().min(1).max(2048),
805
+ rewrite: SERVICE_REWRITE_STORE_SCHEMA.optional(),
806
+ routes: z.array(SERVICE_ROUTE_STORE_SCHEMA).max(3).optional(),
807
+ defaultPort: z.number().int().min(1).max(65535)
808
+ });
809
+ const GROUP_LIMITS_STORE_SCHEMA = z.strictObject({
810
+ maxConcurrency: z.number().int().min(1).optional(),
811
+ dailyRequests: z.number().int().min(1).optional()
812
+ });
813
+ const GROUP_STORE_SCHEMA = z.strictObject({
814
+ name: z.string().min(1).max(256),
815
+ serviceIds: z.array(z.string().min(1).max(128)).max(256),
816
+ limits: GROUP_LIMITS_STORE_SCHEMA.optional()
817
+ });
818
+ const KEY_STORE_SCHEMA = z.strictObject({
819
+ keyId: z.string().min(1).max(128),
820
+ group: z.string().min(1).max(256),
821
+ hash: z.string().regex(/^[0-9a-f]{64}$/, "key hash must be 64 hex chars"),
822
+ createdAt: z.number().int().min(0),
823
+ revokedAt: z.number().int().min(0).optional()
824
+ });
825
+ const STORE_FILE_SCHEMA = z.strictObject({
826
+ revision: z.number().int().min(0),
827
+ meta: z.strictObject({ alias: z.string().min(1).max(256).optional() }).optional(),
828
+ services: z.array(SERVICE_STORE_SCHEMA).max(1024),
829
+ groups: z.array(GROUP_STORE_SCHEMA).max(256),
830
+ keys: z.array(KEY_STORE_SCHEMA).max(1024)
831
+ });
832
+ function ensurePrivateDir(dir) {
833
+ mkdirSync(dir, {
834
+ recursive: true,
835
+ mode: DIR_MODE
836
+ });
837
+ try {
838
+ chmodSync(dir, DIR_MODE);
839
+ } catch {}
840
+ }
841
+ /** 原子写:同目录 tmp + rename(失败残留 tmp 不影响旧文件)。 */
842
+ function atomicWriteFileSync(path, contents, mode = FILE_MODE) {
843
+ const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
844
+ writeFileSync(tmp, contents, { mode });
845
+ try {
846
+ chmodSync(tmp, mode);
847
+ } catch {}
848
+ renameSync(tmp, path);
849
+ }
850
+ function hashKeyMaterial(material) {
851
+ return createHash("sha256").update(KEY_HASH_SALT + material).digest("hex");
852
+ }
853
+ const PRIVILEGED_PORT_MAX = 1023;
854
+ /** 解析并校验上游 URL:http/https、必须有主机名、禁 userinfo/query/fragment。 */
855
+ function parseUpstreamUrl(upstream) {
856
+ let url;
857
+ try {
858
+ url = new URL(upstream);
859
+ } catch {
860
+ throw new StoreError("invalid", `error: invalid upstream URL: ${upstream}`);
861
+ }
862
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new StoreError("invalid", "error: upstream URL scheme must be http or https");
863
+ if (url.username !== "" || url.password !== "") throw new StoreError("invalid", "error: upstream URL must not embed credentials (user:pass@)");
864
+ if (url.hostname === "") throw new StoreError("invalid", "error: upstream URL must have a hostname");
865
+ if (url.search !== "" || url.hash !== "") throw new StoreError("invalid", "error: upstream URL must not contain query or fragment");
866
+ return url;
867
+ }
868
+ /** 上游生效端口(显式或缺省scheme端口)。 */
869
+ function upstreamEffectivePort(url) {
870
+ if (url.port !== "") return Number(url.port);
871
+ return url.protocol === "https:" ? 443 : 80;
872
+ }
873
+ var ProviderStore = class ProviderStore {
874
+ dataDir;
875
+ data;
876
+ constructor(dataDir, data) {
877
+ this.dataDir = dataDir;
878
+ this.data = data;
879
+ }
880
+ static filePath(dataDir) {
881
+ return join(dataDir, "services.json");
882
+ }
883
+ /** 打开(不存在则初始化空存储);损坏/非法文件抛 StoreError(corrupt)。 */
884
+ static open(dataDir) {
885
+ ensurePrivateDir(dataDir);
886
+ const path = ProviderStore.filePath(dataDir);
887
+ if (!existsSync(path)) return new ProviderStore(dataDir, {
888
+ revision: 0,
889
+ services: [],
890
+ groups: [],
891
+ keys: []
892
+ });
893
+ let raw;
894
+ try {
895
+ raw = readFileSync(path, "utf8");
896
+ } catch (err) {
897
+ throw new StoreError("corrupt", `error: cannot read ${path}: ${err.message}`);
898
+ }
899
+ let parsed;
900
+ try {
901
+ parsed = JSON.parse(raw);
902
+ } catch {
903
+ throw new StoreError("corrupt", `error: ${path} is not valid JSON (fix or remove it manually)`);
904
+ }
905
+ const result = STORE_FILE_SCHEMA.safeParse(parsed);
906
+ if (!result.success) throw new StoreError("corrupt", `error: ${path} failed validation: ${result.error.message}`);
907
+ return new ProviderStore(dataDir, result.data);
908
+ }
909
+ /** 当前文件 revision(watcher 判重入用)。 */
910
+ get revision() {
911
+ return this.data.revision;
912
+ }
913
+ listServices() {
914
+ return this.data.services.map((s) => ({ ...s }));
915
+ }
916
+ getService(serviceId) {
917
+ const found = this.data.services.find((s) => s.serviceId === serviceId);
918
+ return found === void 0 ? void 0 : { ...found };
919
+ }
920
+ getServiceByName(name) {
921
+ const found = this.data.services.find((s) => s.name === name);
922
+ return found === void 0 ? void 0 : { ...found };
923
+ }
924
+ addService(input) {
925
+ const name = input.name.trim();
926
+ if (name === "" || name.length > 256) throw new StoreError("invalid", "error: service name must be 1..256 chars");
927
+ if (this.data.services.some((s) => s.name === name)) throw new StoreError("duplicate", `error: service '${name}' already exists`);
928
+ if (input.match.length === 0) throw new StoreError("invalid", "error: service must declare at least one match rule");
929
+ if (input.match.length > 64) throw new StoreError("invalid", "error: service match rules exceed 64 entries");
930
+ for (const rule of input.match) if (rule.type === "regex") try {
931
+ new RegExp(rule.value);
932
+ } catch (err) {
933
+ throw new StoreError("invalid", `error: invalid regex '${rule.value}': ${err.message}`);
934
+ }
935
+ const upstreamUrl = parseUpstreamUrl(input.upstream);
936
+ let defaultPort;
937
+ if (input.defaultPort === void 0) {
938
+ const eff = upstreamEffectivePort(upstreamUrl);
939
+ if (eff <= PRIVILEGED_PORT_MAX) throw new StoreError("invalid", `error: upstream port ${eff} is privileged; declare an explicit consumer-side default port (--port <n>)`);
940
+ defaultPort = eff;
941
+ } else {
942
+ if (!Number.isInteger(input.defaultPort) || input.defaultPort < 1 || input.defaultPort > 65535) throw new StoreError("invalid", "error: default port must be an integer in 1..65535");
943
+ defaultPort = input.defaultPort;
944
+ }
945
+ const rewrite = input.rewrite === void 0 ? void 0 : normalizeRewrite(input.rewrite);
946
+ const routes = normalizeRoutes(input.routes);
947
+ let serviceId;
948
+ do
949
+ serviceId = randomZ32(8);
950
+ while (this.data.services.some((s) => s.serviceId === serviceId));
951
+ const service = {
952
+ serviceId,
953
+ name,
954
+ match: input.match.map((m) => ({ ...m })),
955
+ upstream: upstreamUrl.href,
956
+ rewrite,
957
+ ...routes !== void 0 ? { routes } : {},
958
+ defaultPort
959
+ };
960
+ this.data.services.push(service);
961
+ this.save();
962
+ return { ...service };
963
+ }
964
+ removeService(name) {
965
+ const idx = this.data.services.findIndex((s) => s.name === name);
966
+ if (idx < 0) throw new StoreError("not-found", `error: service '${name}' not found`);
967
+ const [removed] = this.data.services.splice(idx, 1);
968
+ for (const group of this.data.groups) group.serviceIds = group.serviceIds.filter((id) => id !== removed.serviceId);
969
+ this.save();
970
+ }
971
+ listGroups() {
972
+ return this.data.groups.map((g) => ({
973
+ ...g,
974
+ serviceIds: [...g.serviceIds]
975
+ }));
976
+ }
977
+ getGroup(name) {
978
+ const found = this.data.groups.find((g) => g.name === name);
979
+ return found === void 0 ? void 0 : {
980
+ ...found,
981
+ serviceIds: [...found.serviceIds],
982
+ limits: found.limits ? { ...found.limits } : void 0
983
+ };
984
+ }
985
+ /** 组内服务视图(引用不存在的服务Id 自动跳过——防手工编辑残留)。 */
986
+ groupServices(groupName) {
987
+ const group = this.getGroup(groupName);
988
+ if (group === void 0) return [];
989
+ const out = [];
990
+ for (const id of group.serviceIds) {
991
+ const svc = this.getService(id);
992
+ if (svc !== void 0) out.push(svc);
993
+ }
994
+ return out;
995
+ }
996
+ addGroup(name, serviceNames, limits) {
997
+ const trimmed = name.trim();
998
+ if (trimmed === "" || trimmed.length > 256) throw new StoreError("invalid", "error: group name must be 1..256 chars");
999
+ if (this.data.groups.some((g) => g.name === trimmed)) throw new StoreError("duplicate", `error: group '${trimmed}' already exists`);
1000
+ const serviceIds = this.resolveServiceIds(serviceNames);
1001
+ if (limits !== void 0) validateLimits(limits);
1002
+ const group = {
1003
+ name: trimmed,
1004
+ serviceIds,
1005
+ limits: limits === void 0 ? void 0 : { ...limits }
1006
+ };
1007
+ this.data.groups.push(group);
1008
+ this.save();
1009
+ return {
1010
+ ...group,
1011
+ serviceIds: [...serviceIds]
1012
+ };
1013
+ }
1014
+ /** 更新既有分组的服务引用(整表替换;未知服务名报错)。 */
1015
+ setGroupServices(name, serviceNames) {
1016
+ const group = this.data.groups.find((g) => g.name === name);
1017
+ if (group === void 0) throw new StoreError("not-found", `error: group '${name}' not found`);
1018
+ group.serviceIds = this.resolveServiceIds(serviceNames);
1019
+ this.save();
1020
+ return {
1021
+ ...group,
1022
+ serviceIds: [...group.serviceIds]
1023
+ };
1024
+ }
1025
+ /** 更新分组限额(undefined = 清除限额变无限)。 */
1026
+ setGroupLimits(name, limits) {
1027
+ const group = this.data.groups.find((g) => g.name === name);
1028
+ if (group === void 0) throw new StoreError("not-found", `error: group '${name}' not found`);
1029
+ if (limits !== void 0) validateLimits(limits);
1030
+ group.limits = limits === void 0 ? void 0 : { ...limits };
1031
+ this.save();
1032
+ return {
1033
+ ...group,
1034
+ serviceIds: [...group.serviceIds]
1035
+ };
1036
+ }
1037
+ /** 删除分组(仍有未撤销密钥时拒绝——孤儿密钥会以 key_all_invalid 形态困扰
1038
+ * 持钥消费方;先 revoke 再删。Owner 验收 2026-09-10:group 管理对齐 keys)。 */
1039
+ removeGroup(name) {
1040
+ const index = this.data.groups.findIndex((g) => g.name === name);
1041
+ if (index === -1) throw new StoreError("not-found", `error: group '${name}' not found`);
1042
+ const activeKeys = this.data.keys.filter((k) => k.group === name && k.revokedAt === void 0);
1043
+ if (activeKeys.length > 0) throw new StoreError("conflict", `error: group '${name}' still has ${activeKeys.length} active key(s) - revoke them first`);
1044
+ this.data.groups.splice(index, 1);
1045
+ this.save();
1046
+ }
1047
+ resolveServiceIds(serviceNames) {
1048
+ const serviceIds = [];
1049
+ for (const svcName of serviceNames) {
1050
+ const svc = this.getServiceByName(svcName);
1051
+ if (svc === void 0) throw new StoreError("not-found", `error: service '${svcName}' not found`);
1052
+ if (!serviceIds.includes(svc.serviceId)) serviceIds.push(svc.serviceId);
1053
+ }
1054
+ return serviceIds;
1055
+ }
1056
+ listKeys() {
1057
+ return this.data.keys.map((k) => ({ ...k }));
1058
+ }
1059
+ /** 签发:原文仅本次返回,存储只落哈希。 */
1060
+ issueKey(groupName) {
1061
+ if (this.getGroup(groupName) === void 0) throw new StoreError("not-found", `error: group '${groupName}' not found`);
1062
+ let keyId;
1063
+ do
1064
+ keyId = randomZ32(8);
1065
+ while (this.data.keys.some((k) => k.keyId === keyId));
1066
+ const key = KEY_MATERIAL_PREFIX + randomZ32(32);
1067
+ const createdAt = Date.now();
1068
+ this.data.keys.push({
1069
+ keyId,
1070
+ group: groupName,
1071
+ hash: hashKeyMaterial(key),
1072
+ createdAt
1073
+ });
1074
+ this.save();
1075
+ return {
1076
+ keyId,
1077
+ key,
1078
+ createdAt
1079
+ };
1080
+ }
1081
+ /** 撤销(幂等:已撤销为 no-op)。 */
1082
+ revokeKey(keyId) {
1083
+ const found = this.data.keys.find((k) => k.keyId === keyId);
1084
+ if (found === void 0) throw new StoreError("not-found", `error: key '${keyId}' not found`);
1085
+ if (found.revokedAt === void 0) {
1086
+ found.revokedAt = Date.now();
1087
+ this.save();
1088
+ }
1089
+ return { ...found };
1090
+ }
1091
+ /**
1092
+ * 校验密钥原文:SHA-256(salt+原文) 后对全表 timingSafeEqual 扫描(不因命中
1093
+ * 提前退出,时序与表内容无关节)。命中后按 revokedAt 区分 valid/revoked。
1094
+ */
1095
+ verifyKey(material) {
1096
+ const digest = Buffer.from(hashKeyMaterial(material), "hex");
1097
+ let match;
1098
+ for (const record of this.data.keys) {
1099
+ const stored = Buffer.from(record.hash, "hex");
1100
+ if (digest.length === stored.length && timingSafeEqual(digest, stored)) match = record;
1101
+ }
1102
+ if (match === void 0) return { status: "invalid" };
1103
+ const located = {
1104
+ keyId: match.keyId,
1105
+ group: match.group
1106
+ };
1107
+ return match.revokedAt === void 0 ? {
1108
+ status: "valid",
1109
+ ...located
1110
+ } : {
1111
+ status: "revoked",
1112
+ ...located
1113
+ };
1114
+ }
1115
+ get alias() {
1116
+ return this.data.meta?.alias;
1117
+ }
1118
+ setAlias(alias) {
1119
+ const trimmed = alias.trim();
1120
+ if (trimmed === "" || trimmed.length > 256) throw new StoreError("invalid", "error: alias must be 1..256 chars");
1121
+ this.data.meta = {
1122
+ ...this.data.meta ?? {},
1123
+ alias: trimmed
1124
+ };
1125
+ this.save();
1126
+ }
1127
+ /** 只读快照(引擎/测试)。 */
1128
+ snapshot() {
1129
+ return JSON.parse(JSON.stringify(this.data));
1130
+ }
1131
+ save() {
1132
+ this.data.revision += 1;
1133
+ atomicWriteFileSync(ProviderStore.filePath(this.dataDir), `${JSON.stringify(this.data, null, 2)}\n`);
1134
+ }
1135
+ };
1136
+ function validateLimits(limits) {
1137
+ for (const [key, value] of Object.entries(limits)) if (value !== void 0 && (!Number.isInteger(value) || value < 1)) throw new StoreError("invalid", `error: limit '${key}' must be a positive integer`);
1138
+ }
1139
+ /**
1140
+ * 路由表规范化(M3-r7):空表→undefined;模式字段按 mode 归一——
1141
+ * prefix:localPrefix 补 / 去尾斜杠(缺省按首个 form 规范前缀)+ upstreamPrefix;
1142
+ * pattern:matchPattern 编译校验({name}→:name 兼容翻译)+ template RFC 6570
1143
+ * 解析校验(写入期 fail-fast,运行期零重复解析)。不做语义限制。
1144
+ */
1145
+ function normalizeRoutes(routes) {
1146
+ if (routes === void 0) return void 0;
1147
+ const cleaned = routes.filter((r) => r !== null && r !== void 0);
1148
+ if (cleaned.length === 0) return void 0;
1149
+ const out = [];
1150
+ for (const route of cleaned) {
1151
+ const forms = [...new Set(route.forms ?? [])];
1152
+ if (route.mode === "pattern") {
1153
+ const matchPattern = route.matchPattern?.trim() ?? "";
1154
+ const template = route.template?.trim() ?? "";
1155
+ if (matchPattern === "" || template === "") throw new StoreError("invalid", "error: pattern route requires matchPattern and template");
1156
+ try {
1157
+ compileMatchPattern(matchPattern);
1158
+ } catch (err) {
1159
+ throw new StoreError("invalid", `error: invalid matchPattern '${matchPattern}': ${err.message}`);
1160
+ }
1161
+ try {
1162
+ validateUriTemplate(template);
1163
+ } catch (err) {
1164
+ throw new StoreError("invalid", `error: invalid template '${template}': ${err.message}`);
1165
+ }
1166
+ out.push({
1167
+ forms,
1168
+ mode: "pattern",
1169
+ matchPattern,
1170
+ template
1171
+ });
1172
+ continue;
1173
+ }
1174
+ let local = route.localPrefix?.trim() ?? "";
1175
+ if (local !== "") {
1176
+ if (!local.startsWith("/")) local = `/${local}`;
1177
+ local = local.replace(/\/+$/, "");
1178
+ }
1179
+ if (local === "") local = ROUTE_LOCAL_PREFIX[forms[0] ?? "openai-chat"];
1180
+ let up = route.upstreamPrefix?.trim() ?? "";
1181
+ if (up !== "") {
1182
+ if (!up.startsWith("/")) up = `/${up}`;
1183
+ up = up.replace(/\/+$/, "");
1184
+ }
1185
+ out.push({
1186
+ forms,
1187
+ localPrefix: local,
1188
+ upstreamPrefix: up
1189
+ });
1190
+ }
1191
+ return out;
1192
+ }
1193
+ function normalizeRewrite(rewrite) {
1194
+ const out = {};
1195
+ if (rewrite.hostHeader !== void 0) {
1196
+ if (rewrite.hostHeader.trim() === "") throw new StoreError("invalid", "error: rewrite hostHeader must not be empty");
1197
+ out.hostHeader = rewrite.hostHeader.trim();
1198
+ }
1199
+ for (const field of ["pathPrefixStrip", "pathPrefixAppend"]) {
1200
+ const value = rewrite[field];
1201
+ if (value === void 0) continue;
1202
+ const norm = value.startsWith("/") ? value : `/${value}`;
1203
+ if (norm === "/") throw new StoreError("invalid", `error: rewrite ${field} must not be '/'`);
1204
+ out[field] = norm;
1205
+ }
1206
+ if (rewrite.headerSet !== void 0) {
1207
+ const headerSet = {};
1208
+ for (const [name, value] of Object.entries(rewrite.headerSet)) {
1209
+ const lower = name.toLowerCase();
1210
+ if (lower === "") throw new StoreError("invalid", "error: rewrite headerSet name must not be empty");
1211
+ headerSet[lower] = value;
1212
+ }
1213
+ out.headerSet = headerSet;
1214
+ }
1215
+ if (rewrite.headerRemove !== void 0) out.headerRemove = [...new Set(rewrite.headerRemove.map((n) => n.toLowerCase()))];
1216
+ return out;
1217
+ }
1218
+ //#endregion
1219
+ //#region src/cli/commands/provider/common.ts
1220
+ function resolveDataDir(flag, home = homedir()) {
1221
+ if (flag === void 0 || flag === "") return join(home, ".aifly", "provider");
1222
+ return flag;
1223
+ }
1224
+ /** relay 解析(flag > AIFLY_RELAY env > config file);exactOptionalPropertyTypes 安全组装。 */
1225
+ function resolvedRelayUrls(options, home) {
1226
+ const input = {};
1227
+ if (Array.isArray(options.relay)) input.flag = options.relay;
1228
+ const env = process.env.AIFLY_RELAY;
1229
+ if (env !== void 0) input.env = env;
1230
+ input.file = loadConfig(home);
1231
+ return resolveRelayUrls(input);
1232
+ }
1233
+ function str(value) {
1234
+ if (typeof value === "string") return value;
1235
+ }
1236
+ function multi(value) {
1237
+ if (Array.isArray(value)) return value;
1238
+ if (typeof value === "string") return [value];
1239
+ return [];
1240
+ }
1241
+ function requireString(value, label) {
1242
+ if (value === void 0 || value === "") throw new UsageError(`error: missing required option --${label}`);
1243
+ return value;
1244
+ }
1245
+ function parsePositiveInt(raw, label) {
1246
+ const n = Number(raw);
1247
+ if (!Number.isInteger(n) || n < 1 || n > Number.MAX_SAFE_INTEGER) throw new UsageError(`error: invalid ${label} value: ${raw} (expected a positive integer)`);
1248
+ return n;
1249
+ }
1250
+ function parsePortNumber(raw, label) {
1251
+ const n = parsePositiveInt(raw, label);
1252
+ if (n > 65535) throw new UsageError(`error: invalid ${label} value: ${raw} (expected 1..65535)`);
1253
+ return n;
1254
+ }
1255
+ /** "type:value"(value 为剩余整体;type in exact|suffix|regex)。 */
1256
+ function parseMatchSpec(raw) {
1257
+ const idx = raw.indexOf(":");
1258
+ if (idx <= 0) throw new UsageError(`error: invalid --match value: ${raw} (expected <exact|suffix|regex>:<value>)`);
1259
+ const type = raw.slice(0, idx);
1260
+ const value = raw.slice(idx + 1);
1261
+ if (type !== "exact" && type !== "suffix" && type !== "regex") throw new UsageError(`error: invalid --match type: ${type} (expected exact|suffix|regex)`);
1262
+ if (value === "") throw new UsageError(`error: invalid --match value: ${raw} (value must not be empty)`);
1263
+ return {
1264
+ type,
1265
+ value
1266
+ };
1267
+ }
1268
+ /** "Name=value"(首个 = 分割;Name 小写化)。 */
1269
+ function parseHeaderSetSpec(raw) {
1270
+ const idx = raw.indexOf("=");
1271
+ if (idx <= 0) throw new UsageError(`error: invalid --header-set value: ${raw} (expected <name>=<value>, value may be $env:VAR)`);
1272
+ return {
1273
+ name: raw.slice(0, idx).trim().toLowerCase(),
1274
+ value: raw.slice(idx + 1)
1275
+ };
1276
+ }
1277
+ /** 各命令的 rewrite 组装(仅在存在任一重写选项时构造)。 */
1278
+ function buildRewrite(input) {
1279
+ const rewrite = {};
1280
+ if (input.host !== void 0) rewrite.hostHeader = input.host;
1281
+ if (input.strip !== void 0) rewrite.pathPrefixStrip = input.strip;
1282
+ if (input.append !== void 0) rewrite.pathPrefixAppend = input.append;
1283
+ if (input.headerSet.length > 0) {
1284
+ const headerSet = {};
1285
+ for (const h of input.headerSet) headerSet[h.name] = h.value;
1286
+ rewrite.headerSet = headerSet;
1287
+ }
1288
+ if (input.headerRemove.length > 0) rewrite.headerRemove = input.headerRemove;
1289
+ return Object.keys(rewrite).length === 0 ? void 0 : rewrite;
1290
+ }
1291
+ function openStore(dataDir) {
1292
+ return ProviderStore.open(dataDir);
1293
+ }
1294
+ //#endregion
1295
+ export { expandUriTemplate as _, parseMatchSpec as a, SECRET_NAME_SCHEMA as b, requireString as c, str as d, ProviderStore as f, parseUpstreamUrl as g, ensurePrivateDir as h, parseHeaderSetSpec as i, resolveDataDir as l, atomicWriteFileSync as m, multi as n, parsePortNumber as o, StoreError as p, openStore as r, parsePositiveInt as s, buildRewrite as t, resolvedRelayUrls as u, compileMatchPattern as v, routeLocalPrefix as x, matchRequestPath as y };