@tokenbuddy/tb-admin 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/src/bootstrap-registry.d.ts +1 -0
  2. package/dist/src/bootstrap-registry.d.ts.map +1 -1
  3. package/dist/src/bootstrap-registry.js.map +1 -1
  4. package/dist/src/cli.d.ts.map +1 -1
  5. package/dist/src/cli.js +30 -16
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/server-cmd.d.ts +27 -2
  8. package/dist/src/server-cmd.d.ts.map +1 -1
  9. package/dist/src/server-cmd.js +131 -26
  10. package/dist/src/server-cmd.js.map +1 -1
  11. package/dist/src/ui-actions.d.ts +88 -0
  12. package/dist/src/ui-actions.d.ts.map +1 -0
  13. package/dist/src/ui-actions.js +763 -0
  14. package/dist/src/ui-actions.js.map +1 -0
  15. package/dist/src/ui-command.d.ts +4 -0
  16. package/dist/src/ui-command.d.ts.map +1 -0
  17. package/dist/src/ui-command.js +37 -0
  18. package/dist/src/ui-command.js.map +1 -0
  19. package/dist/src/ui-server.d.ts +23 -0
  20. package/dist/src/ui-server.d.ts.map +1 -0
  21. package/dist/src/ui-server.js +245 -0
  22. package/dist/src/ui-server.js.map +1 -0
  23. package/dist/src/ui-state.d.ts +134 -0
  24. package/dist/src/ui-state.d.ts.map +1 -0
  25. package/dist/src/ui-state.js +407 -0
  26. package/dist/src/ui-state.js.map +1 -0
  27. package/dist/src/ui-static.d.ts +2 -0
  28. package/dist/src/ui-static.d.ts.map +1 -0
  29. package/dist/src/ui-static.js +144 -0
  30. package/dist/src/ui-static.js.map +1 -0
  31. package/dist/src/upstream-balance-probe.d.ts +41 -0
  32. package/dist/src/upstream-balance-probe.d.ts.map +1 -0
  33. package/dist/src/upstream-balance-probe.js +379 -0
  34. package/dist/src/upstream-balance-probe.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/bootstrap-registry.ts +1 -0
  37. package/src/cli.ts +32 -16
  38. package/src/server-cmd.ts +163 -29
  39. package/src/ui-actions.ts +901 -0
  40. package/src/ui-command.ts +39 -0
  41. package/src/ui-server.ts +308 -0
  42. package/src/ui-state.ts +575 -0
  43. package/src/ui-static.ts +144 -0
  44. package/src/upstream-balance-probe.ts +505 -0
  45. package/tests/admin.test.ts +893 -4
@@ -0,0 +1,763 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import YAML from "js-yaml";
6
+ import { AdminUiState } from "./ui-state.js";
7
+ export class UiActions {
8
+ state;
9
+ options;
10
+ commandRunner;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.commandRunner = options.commandRunner;
14
+ this.state = new AdminUiState(options);
15
+ }
16
+ async updateSellerConfig(id, patch) {
17
+ const { profileName, config } = await this.state.rawSellerConfig(id);
18
+ if (!profileName) {
19
+ throw new Error(`seller \`${id}\` has no local profile for seller-config put`);
20
+ }
21
+ const next = mergeSellerConfigPatch(config, patch);
22
+ return this.withTempYaml(next, async (filePath) => {
23
+ const validate = await this.runAdmin(profileArgs(this.options, profileName, ["seller-config", "validate", "--file", filePath]), 30000);
24
+ if (!validate.ok) {
25
+ return validate;
26
+ }
27
+ return this.runAdmin(profileArgs(this.options, profileName, ["seller-config", "put", "--file", filePath]), 30000);
28
+ });
29
+ }
30
+ async createSeller(request, progress) {
31
+ const normalizedRequest = normalizeCreateSellerRequest(request);
32
+ const report = progress || (() => undefined);
33
+ validateCreateSellerRequest(normalizedRequest);
34
+ report({ stepId: "check_registry", status: "running", title: "Check bootstrap registry", message: "Checking seller name conflicts before creating Fly resources." });
35
+ const registry = await this.state.fetchRegistry();
36
+ const appName = normalizedRequest.app || normalizedRequest.sellerName;
37
+ const conflict = registry.sellers.find((seller) => {
38
+ return seller.id === normalizedRequest.sellerName || seller.name === normalizedRequest.sellerName || seller.app === appName;
39
+ });
40
+ if (conflict) {
41
+ throw new Error(`seller \`${normalizedRequest.sellerName}\` already exists in bootstrap registry`);
42
+ }
43
+ report({ stepId: "check_registry", status: "succeeded", title: "Check bootstrap registry", message: "No registry conflict found." });
44
+ const provider = this.options.configManager.getSellerProvider("fly");
45
+ const operatorSecret = normalizedRequest.operatorSecret || provider?.operator_secret;
46
+ const flyConfig = normalizedRequest.flyConfig;
47
+ if (!operatorSecret) {
48
+ throw new Error("operatorSecret is required in local admin config seller_providers.fly.operator_secret or request body");
49
+ }
50
+ if (!flyConfig) {
51
+ throw new Error("flyConfig is required before creating a seller deployment");
52
+ }
53
+ const configRequest = {
54
+ ...normalizedRequest,
55
+ operatorSecret
56
+ };
57
+ return this.withTempYaml(initialSellerConfig(configRequest, false), async (filePath) => {
58
+ const normalizedUpstreamMessage = normalizedRequest.upstreamUrl !== request.upstreamUrl
59
+ ? `Normalized upstreamUrl to ${normalizedRequest.upstreamUrl} before validation.`
60
+ : "";
61
+ report({ stepId: "validate_config", status: "running", title: "Validate seller config", message: normalizedUpstreamMessage || "Validating generated seller config." });
62
+ const configValidation = await this.runAdmin(globalArgs(this.options, ["seller-config", "validate", "--file", filePath]), 30000);
63
+ report({
64
+ stepId: "validate_config",
65
+ status: configValidation.ok ? "succeeded" : "failed",
66
+ title: "Validate seller config",
67
+ message: configValidation.ok ? `${normalizedUpstreamMessage} Seller config is valid.`.trim() : "Seller config validation failed.",
68
+ result: configValidation
69
+ });
70
+ if (!configValidation.ok) {
71
+ return {
72
+ result: configValidation,
73
+ configValidation,
74
+ configPreview: initialSellerConfig(configRequest, true),
75
+ publishRegistry: "skipped"
76
+ };
77
+ }
78
+ const args = [
79
+ "seller",
80
+ "create",
81
+ normalizedRequest.sellerName,
82
+ "--region",
83
+ normalizedRequest.region,
84
+ "--image",
85
+ normalizedRequest.image,
86
+ "--fly-config",
87
+ flyConfig,
88
+ "--initial-config",
89
+ filePath,
90
+ "--operator-secret",
91
+ operatorSecret
92
+ ];
93
+ if (normalizedRequest.app) {
94
+ args.push("--app", normalizedRequest.app);
95
+ }
96
+ if (normalizedRequest.dryRun) {
97
+ args.push("--dry-run");
98
+ }
99
+ report({ stepId: "create_deployment", status: "running", title: "Create Fly deployment", message: `Creating ${appName} in ${normalizedRequest.region}.` });
100
+ const result = await this.runAdmin(globalArgs(this.options, args), 10 * 60 * 1000);
101
+ report({
102
+ stepId: "create_deployment",
103
+ status: result.ok ? "succeeded" : "failed",
104
+ title: "Create Fly deployment",
105
+ message: result.ok ? "Fly deployment command completed." : "Fly deployment command failed.",
106
+ result
107
+ });
108
+ let configPut;
109
+ let modelsRefresh;
110
+ let readiness;
111
+ if (result.ok && !normalizedRequest.dryRun) {
112
+ readiness = await this.waitForSellerReady(appName, operatorSecret, report);
113
+ if (!readiness.ok) {
114
+ return {
115
+ result,
116
+ configValidation,
117
+ readiness,
118
+ configPreview: initialSellerConfig(configRequest, true),
119
+ publishRegistry: "skipped"
120
+ };
121
+ }
122
+ report({ stepId: "apply_config", status: "running", title: "Apply seller config", message: `Writing config to ${sellerOperatorUrl(appName)}.` });
123
+ configPut = await this.runAdmin(globalArgs({
124
+ ...this.options,
125
+ profile: undefined,
126
+ url: sellerOperatorUrl(appName),
127
+ token: operatorSecret
128
+ }, ["seller-config", "put", "--file", filePath]), 30000);
129
+ report({
130
+ stepId: "apply_config",
131
+ status: configPut.ok ? "succeeded" : "failed",
132
+ title: "Apply seller config",
133
+ message: configPut.ok ? "Seller config was written to the deployment." : "Seller config write failed.",
134
+ result: configPut
135
+ });
136
+ if (configPut.ok) {
137
+ report({ stepId: "refresh_models", status: "running", title: "Refresh upstream models", message: `Refreshing model catalog from ${sellerOperatorUrl(appName)}.` });
138
+ modelsRefresh = await this.runAdmin(globalArgs({
139
+ ...this.options,
140
+ profile: undefined,
141
+ url: sellerOperatorUrl(appName),
142
+ token: operatorSecret
143
+ }, ["upstreams", "refresh", "--auto-models"]), 30000);
144
+ report({
145
+ stepId: "refresh_models",
146
+ status: modelsRefresh.ok ? "succeeded" : "failed",
147
+ title: "Refresh upstream models",
148
+ message: modelsRefresh.ok ? "Upstream model catalog was refreshed." : "Upstream model refresh failed.",
149
+ result: modelsRefresh
150
+ });
151
+ }
152
+ }
153
+ else if (normalizedRequest.dryRun) {
154
+ report({ stepId: "apply_config", status: "skipped", title: "Apply seller config", message: "Skipped because this is a dry run." });
155
+ report({ stepId: "refresh_models", status: "skipped", title: "Refresh upstream models", message: "Skipped because this is a dry run." });
156
+ }
157
+ const registryPublish = await this.publishCreatedSellerRegistryEntry({
158
+ registry,
159
+ request: normalizedRequest,
160
+ appName,
161
+ operatorSecret,
162
+ enabled: Boolean(result.ok && !normalizedRequest.dryRun && readiness?.ok && configPut?.ok && modelsRefresh?.ok),
163
+ report
164
+ });
165
+ return {
166
+ result,
167
+ configValidation,
168
+ readiness,
169
+ configPut,
170
+ modelsRefresh,
171
+ registryPublish,
172
+ configPreview: initialSellerConfig(configRequest, true),
173
+ publishRegistry: registryPublish ? (registryPublish.ok ? "completed" : "failed") : "skipped"
174
+ };
175
+ });
176
+ }
177
+ async setRegistryStatus(id, status) {
178
+ const registry = await this.state.fetchRegistry();
179
+ const target = registry.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
180
+ if (!target) {
181
+ throw new Error(`seller \`${id}\` not found in bootstrap registry`);
182
+ }
183
+ const next = {
184
+ ...registry,
185
+ updatedAt: new Date().toISOString(),
186
+ sellers: registry.sellers.map((seller) => seller === target ? { ...seller, status } : seller)
187
+ };
188
+ return this.withTempJson(next, async (filePath) => {
189
+ const validate = await this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "validate", "--file", filePath]), 30000);
190
+ if (!validate.ok) {
191
+ return validate;
192
+ }
193
+ return this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "put", "--file", filePath]), 30000);
194
+ });
195
+ }
196
+ async deleteDeployment(id, confirm) {
197
+ const registry = await this.state.fetchRegistry();
198
+ const target = registry.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
199
+ if (!target) {
200
+ throw new Error(`seller \`${id}\` not found in bootstrap registry`);
201
+ }
202
+ const app = target.app || target.id || target.name;
203
+ const args = ["seller", "remove", app, "--app", app];
204
+ if (!confirm) {
205
+ args.push("--dry-run");
206
+ }
207
+ return this.runAdmin(globalArgs(this.options, args), confirm ? 10 * 60 * 1000 : 30000);
208
+ }
209
+ async withTempYaml(document, callback) {
210
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tb-admin-ui-"));
211
+ const filePath = path.join(dir, "seller-config.yaml");
212
+ try {
213
+ fs.writeFileSync(filePath, YAML.dump(document, { lineWidth: 120, noRefs: true, sortKeys: false }), "utf8");
214
+ return await callback(filePath);
215
+ }
216
+ finally {
217
+ fs.rmSync(dir, { recursive: true, force: true });
218
+ }
219
+ }
220
+ async withTempJson(document, callback) {
221
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tb-admin-ui-"));
222
+ const filePath = path.join(dir, "registry.json");
223
+ try {
224
+ fs.writeFileSync(filePath, JSON.stringify(document, null, 2), "utf8");
225
+ return await callback(filePath);
226
+ }
227
+ finally {
228
+ fs.rmSync(dir, { recursive: true, force: true });
229
+ }
230
+ }
231
+ runAdmin(args, timeoutMs) {
232
+ if (this.commandRunner) {
233
+ return this.commandRunner(args, timeoutMs);
234
+ }
235
+ return runTbAdmin(args, timeoutMs);
236
+ }
237
+ async waitForSellerReady(appName, operatorSecret, report) {
238
+ const maxAttempts = 18;
239
+ const delayMs = 5000;
240
+ let last;
241
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
242
+ report({
243
+ stepId: "wait_seller",
244
+ status: "running",
245
+ title: "Wait for seller service",
246
+ message: `Waiting for operator API to become ready (${attempt}/${maxAttempts}).`
247
+ });
248
+ last = await this.runAdmin(globalArgs({
249
+ ...this.options,
250
+ profile: undefined,
251
+ url: sellerOperatorUrl(appName),
252
+ token: operatorSecret
253
+ }, ["status"]), 30000);
254
+ if (last.ok) {
255
+ report({
256
+ stepId: "wait_seller",
257
+ status: "succeeded",
258
+ title: "Wait for seller service",
259
+ message: "Seller operator API is ready.",
260
+ result: last
261
+ });
262
+ return last;
263
+ }
264
+ if (attempt < maxAttempts) {
265
+ await sleep(delayMs);
266
+ }
267
+ }
268
+ const failed = last || {
269
+ ok: false,
270
+ stdout: "",
271
+ stderr: "seller service did not become ready",
272
+ command: []
273
+ };
274
+ report({
275
+ stepId: "wait_seller",
276
+ status: "failed",
277
+ title: "Wait for seller service",
278
+ message: "Seller operator API did not become ready before config write.",
279
+ result: failed
280
+ });
281
+ return failed;
282
+ }
283
+ async publishCreatedSellerRegistryEntry(options) {
284
+ const { registry, request, appName, operatorSecret, enabled, report } = options;
285
+ if (!enabled) {
286
+ report({
287
+ stepId: "publish_registry",
288
+ status: "skipped",
289
+ title: "Publish bootstrap registry",
290
+ message: "Skipped because the deployment workflow did not complete successfully."
291
+ });
292
+ return undefined;
293
+ }
294
+ report({
295
+ stepId: "publish_registry",
296
+ status: "running",
297
+ title: "Publish bootstrap registry",
298
+ message: `Publishing ${request.sellerName} to bootstrap registry.`
299
+ });
300
+ let next;
301
+ try {
302
+ const upstreams = await this.fetchSellerOperatorJson(appName, operatorSecret, "/operator/admin/upstreams");
303
+ const service = await this.fetchSellerOperatorJson(appName, operatorSecret, "/operator/admin/service");
304
+ next = nextRegistryDocument(registry, request, appName, upstreams, service);
305
+ }
306
+ catch (err) {
307
+ const failed = {
308
+ ok: false,
309
+ stdout: "",
310
+ stderr: redactSensitive(err.message || "failed to build bootstrap registry entry"),
311
+ command: ["fetch", `${sellerOperatorUrl(appName)}/operator/admin/upstreams`]
312
+ };
313
+ report({
314
+ stepId: "publish_registry",
315
+ status: "failed",
316
+ title: "Publish bootstrap registry",
317
+ message: "Failed to read seller metadata for bootstrap registry.",
318
+ result: failed
319
+ });
320
+ return failed;
321
+ }
322
+ return this.withTempJson(next, async (filePath) => {
323
+ const validate = await this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "validate", "--file", filePath]), 30000);
324
+ if (!validate.ok) {
325
+ report({
326
+ stepId: "publish_registry",
327
+ status: "failed",
328
+ title: "Publish bootstrap registry",
329
+ message: "Generated bootstrap registry entry failed validation.",
330
+ result: validate
331
+ });
332
+ return validate;
333
+ }
334
+ const put = await this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "put", "--file", filePath]), 30000);
335
+ report({
336
+ stepId: "publish_registry",
337
+ status: put.ok ? "succeeded" : "failed",
338
+ title: "Publish bootstrap registry",
339
+ message: put.ok ? "Bootstrap registry was published with the new seller." : "Bootstrap registry publish failed.",
340
+ result: put
341
+ });
342
+ return put;
343
+ });
344
+ }
345
+ async fetchSellerOperatorJson(appName, operatorSecret, pathName) {
346
+ const fetchJson = this.options.fetchJson || defaultFetchJson;
347
+ const response = await fetchJson(`${sellerOperatorUrl(appName)}${pathName}`, {
348
+ headers: { Authorization: `Bearer ${operatorSecret}` }
349
+ });
350
+ if (response && typeof response === "object" && !Array.isArray(response)) {
351
+ return response;
352
+ }
353
+ return {};
354
+ }
355
+ }
356
+ function nextRegistryDocument(registry, request, appName, upstreams, service) {
357
+ const entry = registryEntryFromCreatedSeller(request, appName, upstreams, service);
358
+ const sellers = registry.sellers.filter((seller) => {
359
+ return seller.id !== entry.id && seller.name !== entry.name && seller.app !== entry.app;
360
+ });
361
+ return {
362
+ ...registry,
363
+ version: Number(registry.version || 0) + 1,
364
+ updatedAt: new Date().toISOString(),
365
+ sellers: [...sellers, entry]
366
+ };
367
+ }
368
+ function registryEntryFromCreatedSeller(request, appName, upstreams, service) {
369
+ const models = modelIdsFrom(upstreams.models) || modelIdsFrom(service.models);
370
+ if (!models || models.length === 0) {
371
+ throw new Error("seller operator upstreams did not return models for bootstrap registry");
372
+ }
373
+ const supportedProtocols = supportedProtocolsFrom(service.supportedProtocols)
374
+ || supportedProtocolsFrom(upstreams.supportedProtocols)
375
+ || ["chat_completions"];
376
+ const paymentMethods = paymentMethodsFromRequest(request);
377
+ return {
378
+ id: request.sellerName,
379
+ name: request.sellerName,
380
+ app: appName,
381
+ url: sellerOperatorUrl(appName),
382
+ status: "pending",
383
+ region: stringValue(request.region),
384
+ modelsCount: models.length || undefined,
385
+ sampleModels: models.slice(0, 5),
386
+ models,
387
+ supportedProtocols,
388
+ paymentMethods,
389
+ recommendedFor: models.slice(0, 5)
390
+ };
391
+ }
392
+ function modelIdsFrom(value) {
393
+ if (!Array.isArray(value)) {
394
+ return undefined;
395
+ }
396
+ return Array.from(new Set(value.map((entry) => {
397
+ if (typeof entry === "string") {
398
+ return entry;
399
+ }
400
+ if (entry && typeof entry === "object" && "id" in entry) {
401
+ const id = entry.id;
402
+ return typeof id === "string" && id.trim() ? id.trim() : undefined;
403
+ }
404
+ return undefined;
405
+ }).filter((entry) => Boolean(entry))));
406
+ }
407
+ function supportedProtocolsFrom(value) {
408
+ if (!Array.isArray(value)) {
409
+ return undefined;
410
+ }
411
+ const protocols = value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry));
412
+ return protocols.length > 0 ? Array.from(new Set(protocols)) : undefined;
413
+ }
414
+ export function runTbAdmin(args, timeoutMs) {
415
+ const entry = adminEntryPath();
416
+ const command = [process.execPath, entry, ...args];
417
+ return new Promise((resolve) => {
418
+ const child = spawn(process.execPath, [entry, ...args], {
419
+ shell: false,
420
+ stdio: ["ignore", "pipe", "pipe"]
421
+ });
422
+ const timer = setTimeout(() => {
423
+ child.kill("SIGTERM");
424
+ }, timeoutMs);
425
+ let stdout = "";
426
+ let stderr = "";
427
+ child.stdout.on("data", (chunk) => {
428
+ stdout += chunk.toString();
429
+ });
430
+ child.stderr.on("data", (chunk) => {
431
+ stderr += chunk.toString();
432
+ });
433
+ child.on("close", (code) => {
434
+ clearTimeout(timer);
435
+ resolve({
436
+ ok: code === 0,
437
+ stdout: redactSensitive(stdout),
438
+ stderr: redactSensitive(stderr),
439
+ command: redactCommand(command)
440
+ });
441
+ });
442
+ });
443
+ }
444
+ async function defaultFetchJson(url, init) {
445
+ const response = await fetch(url, init);
446
+ if (!response.ok) {
447
+ const text = await response.text();
448
+ throw new Error(`HTTP Error ${response.status}: ${redactSensitive(text || response.statusText)}`);
449
+ }
450
+ const text = await response.text();
451
+ return text ? JSON.parse(text) : {};
452
+ }
453
+ function adminEntryPath() {
454
+ const current = process.argv[1] || "";
455
+ if (current.endsWith("tb-admin.js")) {
456
+ return current;
457
+ }
458
+ return path.resolve(process.cwd(), "packages/admin-cli/bin/tb-admin.js");
459
+ }
460
+ function globalArgs(options, args) {
461
+ const output = [];
462
+ if (options.configPath) {
463
+ output.push("--config", options.configPath);
464
+ }
465
+ if (options.profile) {
466
+ output.push("--profile", options.profile);
467
+ }
468
+ if (options.url) {
469
+ output.push("--url", options.url);
470
+ }
471
+ if (options.token) {
472
+ output.push("--token", options.token);
473
+ }
474
+ output.push(...args);
475
+ return output;
476
+ }
477
+ function profileArgs(options, profileName, args) {
478
+ return globalArgs({ ...options, profile: profileName, url: undefined, token: undefined }, args);
479
+ }
480
+ function mergeSellerConfigPatch(config, patch) {
481
+ const next = { ...config };
482
+ copyIfPresent(next, patch, "upstreamUrl");
483
+ copyIfPresent(next, patch, "upstreamApiKey");
484
+ applyBalanceProbePatch(next, patch);
485
+ copyIfPresent(next, patch, "upstreamBalanceUrl");
486
+ copyIfPresent(next, patch, "upstreamUserId");
487
+ copyIfPresent(next, patch, "upstreamRechargeUrl");
488
+ copyIfPresent(next, patch, "markupRatio");
489
+ copyIfPresent(next, patch, "discountRatio");
490
+ copyIfPresent(next, patch, "maxConnections");
491
+ copyIfPresent(next, patch, "maxQueueDepth");
492
+ if (Array.isArray(patch.models)) {
493
+ next.models = patch.models;
494
+ }
495
+ if (patch.modelAliases && typeof patch.modelAliases === "object" && !Array.isArray(patch.modelAliases)) {
496
+ next.modelAliases = patch.modelAliases;
497
+ }
498
+ return next;
499
+ }
500
+ function copyIfPresent(target, source, key) {
501
+ if (source[key] !== undefined) {
502
+ target[key] = source[key];
503
+ }
504
+ }
505
+ function validateCreateSellerRequest(request) {
506
+ requireString(request.sellerName, "sellerName");
507
+ requireString(request.region, "region");
508
+ requireString(request.image, "image");
509
+ requireString(request.upstreamWebsite, "upstreamWebsite");
510
+ requireString(request.upstreamUrl, "upstreamUrl");
511
+ requireString(request.upstreamApiKey, "upstreamApiKey");
512
+ if (stringValue(request.upstreamBalanceProbeTemplate) !== "none") {
513
+ requireString(balanceProbeUrl(request), "upstreamBalanceProbeUrl");
514
+ }
515
+ requirePositiveInteger(request.maxConnections, "maxConnections");
516
+ requirePositiveInteger(request.maxQueueDepth, "maxQueueDepth");
517
+ requireFiniteNumber(request.markupRatio, "markupRatio");
518
+ requireFiniteNumber(request.discountRatio, "discountRatio");
519
+ const paymentMethods = paymentMethodsFromRequest(request);
520
+ const invalidPaymentMethod = paymentMethods.find((method) => !["clawtip", "mock"].includes(method));
521
+ if (invalidPaymentMethod) {
522
+ throw new Error("paymentMethods must contain only clawtip or mock");
523
+ }
524
+ if (paymentMethods.includes("clawtip")) {
525
+ requireString(request.clawtipPayTo, "clawtipPayTo");
526
+ requireString(request.clawtipSm4KeyBase64, "clawtipSm4KeyBase64");
527
+ }
528
+ }
529
+ function normalizeCreateSellerRequest(request) {
530
+ const upstreamUrl = stripTrailingV1(request.upstreamUrl);
531
+ const app = stringValue(request.app) || request.sellerName;
532
+ const normalized = {
533
+ ...request,
534
+ app,
535
+ image: stringValue(request.image) || "registry.fly.io/tb-seller:latest",
536
+ flyConfig: stringValue(request.flyConfig) || "deploy/fly.io/fly.tb-seller.toml",
537
+ upstreamUrl
538
+ };
539
+ if (paymentMethodsFromRequest(normalized).includes("clawtip")) {
540
+ normalized.clawtipSkillSlug = stringValue(normalized.clawtipSkillSlug) || app;
541
+ normalized.clawtipSkillId = stringValue(normalized.clawtipSkillId) || `si-${app}`;
542
+ normalized.clawtipDescription = stringValue(normalized.clawtipDescription) || `TokenBuddy Seller ${request.sellerName}`;
543
+ normalized.clawtipResourceUrl = stringValue(normalized.clawtipResourceUrl) || sellerOperatorUrl(app);
544
+ normalized.clawtipActivationFeeFen = Number(normalized.clawtipActivationFeeFen || 1);
545
+ normalized.clawtipMicrosPerFen = Number(normalized.clawtipMicrosPerFen || 10000);
546
+ }
547
+ return normalized;
548
+ }
549
+ function initialSellerConfig(request, masked) {
550
+ const upstreamBalanceProbe = balanceProbeConfigFromRequest(request);
551
+ const upstreamBalanceUrl = balanceProbeUrl(request);
552
+ const upstreamUserId = balanceProbeUserId(request);
553
+ const upstreamRechargeUrl = balanceProbeRechargeUrl(request);
554
+ const paymentConfig = paymentConfigFromRequest(request, masked);
555
+ return {
556
+ upstreamUrl: request.upstreamUrl,
557
+ upstreamApiKey: masked ? "********" : request.upstreamApiKey,
558
+ upstreamWebsite: request.upstreamWebsite,
559
+ upstreamBalanceUrl,
560
+ upstreamUserId,
561
+ upstreamRechargeUrl,
562
+ upstreamBalanceProbe,
563
+ maxConnections: request.maxConnections,
564
+ maxQueueDepth: request.maxQueueDepth,
565
+ markupRatio: request.markupRatio,
566
+ discountRatio: request.discountRatio,
567
+ operatorSecret: masked ? "********" : stringValue(request.operatorSecret),
568
+ ...paymentConfig
569
+ };
570
+ }
571
+ function paymentConfigFromRequest(request, masked) {
572
+ const paymentMethods = paymentMethodsFromRequest(request);
573
+ const config = {
574
+ allowMock: paymentMethods.includes("mock")
575
+ };
576
+ if (!paymentMethods.includes("clawtip")) {
577
+ return config;
578
+ }
579
+ return {
580
+ ...config,
581
+ clawtip: {
582
+ payTo: stringValue(request.clawtipPayTo),
583
+ sm4KeyBase64: masked ? "********" : stringValue(request.clawtipSm4KeyBase64),
584
+ skillSlug: stringValue(request.clawtipSkillSlug),
585
+ skillId: stringValue(request.clawtipSkillId),
586
+ description: stringValue(request.clawtipDescription),
587
+ resourceUrl: stringValue(request.clawtipResourceUrl),
588
+ activationFeeFen: Number(request.clawtipActivationFeeFen),
589
+ microsPerFen: Number(request.clawtipMicrosPerFen)
590
+ }
591
+ };
592
+ }
593
+ function paymentMethodsFromRequest(request) {
594
+ const values = request.paymentMethods !== undefined
595
+ ? arrayStringValues(request.paymentMethods)
596
+ : legacyPaymentMethodsFromRequest(request);
597
+ return Array.from(new Set(values));
598
+ }
599
+ function legacyPaymentMethodsFromRequest(request) {
600
+ const paymentMethod = stringValue(request.paymentMethod);
601
+ if (paymentMethod === "none") {
602
+ return [];
603
+ }
604
+ return [paymentMethod || "clawtip"];
605
+ }
606
+ function arrayStringValues(value) {
607
+ if (Array.isArray(value)) {
608
+ return value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry));
609
+ }
610
+ const single = stringValue(value);
611
+ return single ? single.split(",").map((entry) => entry.trim()).filter(Boolean) : [];
612
+ }
613
+ function applyBalanceProbePatch(target, patch) {
614
+ const template = stringValue(patch.upstreamBalanceProbeTemplate);
615
+ const url = stringValue(patch.upstreamBalanceProbeUrl) || stringValue(patch.upstreamBalanceUrl);
616
+ const userId = stringValue(patch.upstreamBalanceProbeUserId) || stringValue(patch.upstreamUserId);
617
+ const rechargeUrl = stringValue(patch.upstreamBalanceProbeRechargeUrl) || stringValue(patch.upstreamRechargeUrl);
618
+ if (template === undefined && url === undefined && userId === undefined && rechargeUrl === undefined) {
619
+ return;
620
+ }
621
+ const current = objectValue(target.upstreamBalanceProbe) || {};
622
+ target.upstreamBalanceProbe = {
623
+ ...current,
624
+ ...(template !== undefined ? { template } : {}),
625
+ ...(url !== undefined ? { url } : {}),
626
+ ...(userId !== undefined ? { userId } : {}),
627
+ ...(rechargeUrl !== undefined ? { rechargeUrl } : {})
628
+ };
629
+ if (url !== undefined) {
630
+ target.upstreamBalanceUrl = url;
631
+ }
632
+ if (userId !== undefined) {
633
+ target.upstreamUserId = userId;
634
+ }
635
+ if (rechargeUrl !== undefined) {
636
+ target.upstreamRechargeUrl = rechargeUrl;
637
+ }
638
+ }
639
+ function balanceProbeConfigFromRequest(request) {
640
+ return {
641
+ template: stringValue(request.upstreamBalanceProbeTemplate) || "auto",
642
+ url: balanceProbeUrl(request),
643
+ userId: balanceProbeUserId(request),
644
+ rechargeUrl: balanceProbeRechargeUrl(request)
645
+ };
646
+ }
647
+ function balanceProbeUrl(request) {
648
+ const explicit = stringValue(request.upstreamBalanceProbeUrl) || stringValue(request.upstreamBalanceUrl);
649
+ if (explicit) {
650
+ return explicit;
651
+ }
652
+ const template = stringValue(request.upstreamBalanceProbeTemplate) || "auto";
653
+ const upstreamUrl = stringValue(request.upstreamUrl);
654
+ const host = hostName(upstreamUrl);
655
+ if (template === "none") {
656
+ return undefined;
657
+ }
658
+ if (template === "openrouter") {
659
+ return "https://openrouter.ai/api/v1/credits";
660
+ }
661
+ if (template === "deepseek") {
662
+ return "https://api.deepseek.com/user/balance";
663
+ }
664
+ if (template === "stepfun") {
665
+ return "https://api.stepfun.com/v1/accounts";
666
+ }
667
+ if (template === "novita") {
668
+ return "https://api.novita.ai/v3/user/balance";
669
+ }
670
+ if (template === "siliconflow") {
671
+ return host.endsWith(".com") ? "https://api.siliconflow.com/v1/user/info" : "https://api.siliconflow.cn/v1/user/info";
672
+ }
673
+ if (template === "usage_generic") {
674
+ return upstreamUrl ? usageUrl(upstreamUrl) : undefined;
675
+ }
676
+ if (template === "auto") {
677
+ if (host === "api.deepseek.com") {
678
+ return "https://api.deepseek.com/user/balance";
679
+ }
680
+ if (host === "api.stepfun.ai" || host === "api.stepfun.com") {
681
+ return "https://api.stepfun.com/v1/accounts";
682
+ }
683
+ if (host === "api.siliconflow.cn") {
684
+ return "https://api.siliconflow.cn/v1/user/info";
685
+ }
686
+ if (host === "api.siliconflow.com") {
687
+ return "https://api.siliconflow.com/v1/user/info";
688
+ }
689
+ if (host === "openrouter.ai") {
690
+ return "https://openrouter.ai/api/v1/credits";
691
+ }
692
+ if (host === "api.novita.ai") {
693
+ return "https://api.novita.ai/v3/user/balance";
694
+ }
695
+ return upstreamUrl ? usageUrl(upstreamUrl) : undefined;
696
+ }
697
+ return undefined;
698
+ }
699
+ function balanceProbeUserId(request) {
700
+ return stringValue(request.upstreamBalanceProbeUserId) || stringValue(request.upstreamUserId);
701
+ }
702
+ function balanceProbeRechargeUrl(request) {
703
+ return stringValue(request.upstreamBalanceProbeRechargeUrl) || stringValue(request.upstreamRechargeUrl);
704
+ }
705
+ function stringValue(value) {
706
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
707
+ }
708
+ function objectValue(value) {
709
+ return value && typeof value === "object" && !Array.isArray(value)
710
+ ? value
711
+ : undefined;
712
+ }
713
+ function stripTrailingV1(value) {
714
+ return String(value || "").trim().replace(/\/v1\/?$/i, "");
715
+ }
716
+ function usageUrl(value) {
717
+ const base = String(value || "").trim().replace(/\/+$/, "");
718
+ return /\/v1$/i.test(base) ? `${base}/usage` : `${base}/v1/usage`;
719
+ }
720
+ function hostName(value) {
721
+ try {
722
+ return new URL(String(value || "")).hostname.toLowerCase();
723
+ }
724
+ catch {
725
+ return "";
726
+ }
727
+ }
728
+ function sellerOperatorUrl(appName) {
729
+ return `https://${appName}.fly.dev`;
730
+ }
731
+ function sleep(ms) {
732
+ return new Promise((resolve) => setTimeout(resolve, ms));
733
+ }
734
+ function requireString(value, field) {
735
+ if (typeof value !== "string" || !value.trim()) {
736
+ throw new Error(`${field} is required`);
737
+ }
738
+ }
739
+ function requirePositiveInteger(value, field) {
740
+ const parsed = Number(value);
741
+ if (!Number.isInteger(parsed) || parsed < 1) {
742
+ throw new Error(`${field} must be >= 1`);
743
+ }
744
+ }
745
+ function requireFiniteNumber(value, field) {
746
+ if (!Number.isFinite(Number(value))) {
747
+ throw new Error(`${field} must be a number`);
748
+ }
749
+ }
750
+ function redactCommand(command) {
751
+ return command.map((part, index) => {
752
+ const prev = command[index - 1];
753
+ if (prev === "--operator-secret" || prev === "--token" || prev === "--api-key") {
754
+ return "********";
755
+ }
756
+ return redactSensitive(part);
757
+ });
758
+ }
759
+ function redactSensitive(value) {
760
+ return value.replace(/(sk-[A-Za-z0-9_-]+)/g, "********")
761
+ .replace(/(operatorSecret|upstreamApiKey|token|apiKey|sm4KeyBase64|clawtipSm4KeyBase64)["'=:\s]+([^"',\s]+)/gi, "$1=********");
762
+ }
763
+ //# sourceMappingURL=ui-actions.js.map