@tokenbuddy/tb-admin 1.0.14 → 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 +8 -0
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/server-cmd.d.ts +22 -1
  8. package/dist/src/server-cmd.d.ts.map +1 -1
  9. package/dist/src/server-cmd.js +93 -16
  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 +9 -0
  38. package/src/server-cmd.ts +118 -19
  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 +871 -1
package/src/server-cmd.ts CHANGED
@@ -1,6 +1,25 @@
1
- import { execSync } from "child_process";
1
+ import { execSync, spawnSync, type SpawnSyncReturns } from "child_process";
2
+ import * as fs from "fs";
2
3
  import { SellerProviderConfig } from "./config.js";
3
4
 
5
+ type ExecRunner = (command: string, options?: Parameters<typeof execSync>[1]) => string | Buffer;
6
+ type SpawnRunner = (command: string, args?: string[], options?: Parameters<typeof spawnSync>[2]) => SpawnSyncReturns<string | Buffer>;
7
+
8
+ export interface DockerImageInspection {
9
+ ok: boolean;
10
+ error?: string;
11
+ exitCode?: number;
12
+ }
13
+
14
+ export type ImageInspectRunner = (image: string) => DockerImageInspection;
15
+
16
+ export interface FlyProviderRuntime {
17
+ checkFlyctlInstalled?: (flyctlPath?: string) => boolean;
18
+ execSync?: ExecRunner;
19
+ spawnSync?: SpawnRunner;
20
+ imageInspector?: ImageInspectRunner;
21
+ }
22
+
4
23
  /**
5
24
  * 检查 flyctl 是否在 PATH 中(或在指定路径)。
6
25
  *
@@ -16,6 +35,41 @@ export function checkFlyctlInstalled(flyctlPath?: string): boolean {
16
35
  }
17
36
  }
18
37
 
38
+ export function inspectDockerImage(image: string): DockerImageInspection {
39
+ const result = spawnSync("docker", ["buildx", "imagetools", "inspect", image], {
40
+ encoding: "utf8",
41
+ stdio: ["ignore", "pipe", "pipe"]
42
+ });
43
+ return dockerImageInspectionFromResult(result);
44
+ }
45
+
46
+ export function requirePublishedDockerImage(image: string, inspectImage: ImageInspectRunner = inspectDockerImage): void {
47
+ const inspection = inspectImage(image);
48
+ if (inspection.ok) {
49
+ return;
50
+ }
51
+ const detail = inspection.error ? ` Detail: ${inspection.error.trim()}` : "";
52
+ throw new Error(
53
+ `seller image is not published or is not accessible: ${image}. ` +
54
+ `Publish it first with RELEASE_VERSION=<v> bash scripts/release/all.sh, or pass an existing registry.fly.io/tb-seller:<v> tag. ` +
55
+ `No Fly app was created.${detail}`
56
+ );
57
+ }
58
+
59
+ function dockerImageInspectionFromResult(result: SpawnSyncReturns<string>): DockerImageInspection {
60
+ if (result.error) {
61
+ return { ok: false, error: result.error.message };
62
+ }
63
+ if (result.status !== 0) {
64
+ return {
65
+ ok: false,
66
+ exitCode: result.status ?? undefined,
67
+ error: (result.stderr || result.stdout || "").toString()
68
+ };
69
+ }
70
+ return { ok: true };
71
+ }
72
+
19
73
  export function parseFlyMachineIds(json: string, app: string): string[] {
20
74
  let parsed: unknown;
21
75
  try {
@@ -60,6 +114,7 @@ export interface SellerCreateOptions {
60
114
  volumeSizeGb?: number;
61
115
  volumeId?: string;
62
116
  volumeSnapshotRetentionDays?: number;
117
+ initialConfigPath?: string;
63
118
  dryRun?: boolean;
64
119
  }
65
120
 
@@ -69,9 +124,17 @@ export interface SellerCreateOptions {
69
124
  */
70
125
  export class FlyProvider {
71
126
  private providerConfig?: SellerProviderConfig;
127
+ private readonly runtime: Required<FlyProviderRuntime>;
72
128
 
73
- constructor(providerConfig?: SellerProviderConfig) {
129
+ constructor(providerConfig?: SellerProviderConfig, runtime?: FlyProviderRuntime) {
74
130
  this.providerConfig = providerConfig;
131
+ this.runtime = {
132
+ checkFlyctlInstalled,
133
+ execSync,
134
+ spawnSync,
135
+ imageInspector: inspectDockerImage,
136
+ ...runtime
137
+ };
75
138
  }
76
139
 
77
140
  private get flyctl(): string {
@@ -82,10 +145,10 @@ export class FlyProvider {
82
145
  * List apps on Fly.io
83
146
  */
84
147
  public listApps(): string {
85
- if (!checkFlyctlInstalled(this.flyctl)) {
148
+ if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
86
149
  throw new Error(`\`${this.flyctl}\` is not installed on your system PATH.`);
87
150
  }
88
- return execSync(`${this.flyctl} apps list`, { encoding: "utf8" });
151
+ return this.runtime.execSync(`${this.flyctl} apps list`, { encoding: "utf8" }) as string;
89
152
  }
90
153
 
91
154
  /**
@@ -112,6 +175,7 @@ export class FlyProvider {
112
175
  || 1;
113
176
  const volumeSnapshotRetentionDays = options.volumeSnapshotRetentionDays;
114
177
  const volumeId = options.volumeId;
178
+ const initialConfigPath = options.initialConfigPath;
115
179
 
116
180
  if (!targetImage) {
117
181
  throw new Error("seller create requires --image registry.fly.io/tb-seller:<v>");
@@ -129,35 +193,59 @@ export class FlyProvider {
129
193
  ` Volume: ${volumeName} (${volumeSizeGb}GB)`,
130
194
  ];
131
195
  if (flyConfig) lines.push(` Fly config: ${flyConfig}`);
196
+ if (initialConfigPath) lines.push(` Initial config secret: TOKENBUDDY_SELLER_CONFIG_B64 from ${initialConfigPath}`);
132
197
  if (volumeId) lines.push(` Volume ID: ${volumeId}`);
133
198
  if (volumeSnapshotRetentionDays !== undefined) lines.push(` Volume snapshot retention: ${volumeSnapshotRetentionDays} days`);
134
199
  return lines.join("\n");
135
200
  }
136
201
 
137
- if (!checkFlyctlInstalled(this.flyctl)) {
202
+ if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
138
203
  throw new Error(`\`${this.flyctl}\` is not installed on PATH.`);
139
204
  }
140
205
 
141
206
  if (!operatorSecret) {
142
207
  throw new Error("operator_secret is required. Provide --operator-secret or configure seller_providers.fly.operator_secret");
143
208
  }
209
+ requireReadableFile(flyConfig, "Fly config");
210
+ if (initialConfigPath) {
211
+ requireReadableFile(initialConfigPath, "Initial seller config");
212
+ }
213
+ requirePublishedDockerImage(targetImage, this.runtime.imageInspector);
144
214
 
145
215
  console.log(`[Fly.io] Creating app ${appName}...`);
146
- execSync(`${this.flyctl} apps create ${appName} --machines`, { stdio: "inherit" });
216
+ this.runtime.execSync(`${this.flyctl} apps create ${appName} --machines`, { stdio: "inherit" });
147
217
 
148
218
  console.log(`[Fly.io] Setting secrets...`);
149
- execSync(
150
- `${this.flyctl} secrets set ALLOW_MOCK=false OPERATOR_SECRET=${operatorSecret} --app ${appName}`,
151
- { stdio: "inherit" }
152
- );
219
+ this.importCreateSecrets(appName, operatorSecret, initialConfigPath);
153
220
 
154
221
  console.log(`[Fly.io] Deploying image ${targetImage}...`);
155
- const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --region ${targetRegion} --app ${appName} --now`;
156
- execSync(deployCmd, { stdio: "inherit" });
222
+ const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --primary-region ${targetRegion} --app ${appName} --now`;
223
+ this.runtime.execSync(deployCmd, { stdio: "inherit" });
157
224
 
158
225
  return `Successfully deployed ${appName} on Fly.io`;
159
226
  }
160
227
 
228
+ private importCreateSecrets(appName: string, operatorSecret: string, initialConfigPath: string | undefined): void {
229
+ const lines = [
230
+ "ALLOW_MOCK=false",
231
+ `OPERATOR_SECRET=${operatorSecret}`
232
+ ];
233
+ if (initialConfigPath) {
234
+ const configContent = fs.readFileSync(initialConfigPath, "utf8");
235
+ lines.push(`TOKENBUDDY_SELLER_CONFIG_B64=${Buffer.from(configContent, "utf8").toString("base64")}`);
236
+ }
237
+ const result = this.runtime.spawnSync(this.flyctl, ["secrets", "import", "--stage", "--app", appName], {
238
+ input: `${lines.join("\n")}\n`,
239
+ stdio: ["pipe", "inherit", "inherit"]
240
+ });
241
+ if (result.error) {
242
+ throw result.error;
243
+ }
244
+ if (result.status !== 0) {
245
+ throw new Error(`flyctl secrets import failed with exit code ${result.status}`);
246
+ }
247
+ }
248
+
161
249
  /**
162
250
  * Destroy a seller app on Fly.io.
163
251
  * @param nameOrApp Either a bare name (e.g. "86d81e") or a full app name (e.g. "tbs-86d81e").
@@ -170,12 +258,12 @@ export class FlyProvider {
170
258
  return `[DRY-RUN] Will destroy fly app: ${appName}`;
171
259
  }
172
260
 
173
- if (!checkFlyctlInstalled(this.flyctl)) {
261
+ if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
174
262
  throw new Error(`\`${this.flyctl}\` is not installed.`);
175
263
  }
176
264
 
177
265
  console.log(`[Fly.io] Destroying app ${appName}...`);
178
- execSync(`${this.flyctl} apps destroy ${appName} --yes`, { stdio: "inherit" });
266
+ this.runtime.execSync(`${this.flyctl} apps destroy ${appName} --yes`, { stdio: "inherit" });
179
267
 
180
268
  return `Successfully destroyed ${appName} on Fly.io`;
181
269
  }
@@ -185,10 +273,10 @@ export class FlyProvider {
185
273
  */
186
274
  public statusApp(name: string): string {
187
275
  const appName = name.includes("-") ? name : `tb-seller-${name}`;
188
- if (!checkFlyctlInstalled(this.flyctl)) {
276
+ if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
189
277
  throw new Error(`\`${this.flyctl}\` is not installed.`);
190
278
  }
191
- return execSync(`${this.flyctl} status --app ${appName}`, { encoding: "utf8" });
279
+ return this.runtime.execSync(`${this.flyctl} status --app ${appName}`, { encoding: "utf8" }) as string;
192
280
  }
193
281
 
194
282
  /**
@@ -218,16 +306,16 @@ export class FlyProvider {
218
306
  return lines.join("\n");
219
307
  }
220
308
 
221
- if (!checkFlyctlInstalled(this.flyctl)) {
309
+ if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
222
310
  throw new Error(`\`${this.flyctl}\` is not installed.`);
223
311
  }
224
312
 
225
- const machinesJson = execSync(`${this.flyctl} machines list --app ${app} --json`, { encoding: "utf8" });
313
+ const machinesJson = this.runtime.execSync(`${this.flyctl} machines list --app ${app} --json`, { encoding: "utf8" }) as string;
226
314
  const machineIds = parseFlyMachineIds(machinesJson, app);
227
315
 
228
316
  console.log(`[Fly.io] Updating ${app} image on ${machineIds.length} machine(s)...`);
229
317
  for (const machineId of machineIds) {
230
- execSync(
318
+ this.runtime.execSync(
231
319
  `${this.flyctl} machine update ${machineId} --app ${app} --image ${targetImage} --yes`,
232
320
  { stdio: "inherit" }
233
321
  );
@@ -235,3 +323,14 @@ export class FlyProvider {
235
323
  return `Successfully updated ${app} image`;
236
324
  }
237
325
  }
326
+
327
+ function requireReadableFile(filePath: string, label: string): void {
328
+ if (!fs.existsSync(filePath)) {
329
+ throw new Error(`${label} file does not exist: ${filePath}`);
330
+ }
331
+ const stat = fs.statSync(filePath);
332
+ if (!stat.isFile()) {
333
+ throw new Error(`${label} path is not a file: ${filePath}`);
334
+ }
335
+ fs.accessSync(filePath, fs.constants.R_OK);
336
+ }