apiblaze 0.12.1 → 0.14.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.
Files changed (2) hide show
  1. package/dist/index.js +168 -57
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -51,6 +51,14 @@ var init_types = __esm({
51
51
  });
52
52
 
53
53
  // src/lib/auth.ts
54
+ var auth_exports = {};
55
+ __export(auth_exports, {
56
+ clearCredentials: () => clearCredentials,
57
+ getAccessToken: () => getAccessToken,
58
+ getApiblazeDir: () => getApiblazeDir,
59
+ loadCredentials: () => loadCredentials,
60
+ saveCredentials: () => saveCredentials
61
+ });
54
62
  function saveCredentials(creds) {
55
63
  fs.mkdirSync(APIBLAZE_DIR, { recursive: true });
56
64
  fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), "utf-8");
@@ -82,6 +90,9 @@ function getAccessToken() {
82
90
  }
83
91
  return creds.accessToken;
84
92
  }
93
+ function getApiblazeDir() {
94
+ return APIBLAZE_DIR;
95
+ }
85
96
  var fs, os, path, APIBLAZE_DIR, CREDENTIALS_PATH;
86
97
  var init_auth = __esm({
87
98
  "src/lib/auth.ts"() {
@@ -232,6 +243,65 @@ var init_api = __esm({
232
243
  }
233
244
  });
234
245
 
246
+ // src/lib/team.ts
247
+ var team_exports = {};
248
+ __export(team_exports, {
249
+ resolveLinkedTeam: () => resolveLinkedTeam
250
+ });
251
+ async function resolveLinkedTeam(opts) {
252
+ let teams;
253
+ try {
254
+ teams = await getTeams();
255
+ } catch {
256
+ return opts.preferredId ? { teamId: opts.preferredId } : null;
257
+ }
258
+ if (teams.length === 0) return null;
259
+ const preferred = opts.preferredId ? teams.find((t) => t.teamId === opts.preferredId) : void 0;
260
+ if (preferred && !(opts.promptWhenMultiple && opts.interactive && teams.length > 1)) {
261
+ return { teamId: preferred.teamId, teamName: preferred.name };
262
+ }
263
+ if (preferred) {
264
+ const { default: inquirer3 } = await import("inquirer");
265
+ const { chosen: chosen2 } = await inquirer3.prompt([{
266
+ type: "list",
267
+ name: "chosen",
268
+ message: "Which team do you want to work in?",
269
+ default: preferred.teamId,
270
+ choices: teams.map((t) => ({ name: t.name, value: t.teamId }))
271
+ }]);
272
+ const picked2 = teams.find((t) => t.teamId === chosen2);
273
+ return { teamId: picked2.teamId, teamName: picked2.name };
274
+ }
275
+ if (opts.preferredId) {
276
+ console.log(import_chalk.default.yellow("\nYour previously linked team is no longer available."));
277
+ }
278
+ if (teams.length === 1) {
279
+ console.log(`${import_chalk.default.cyan("\u2192")} Linking to your team ${import_chalk.default.bold(teams[0].name)}.`);
280
+ return { teamId: teams[0].teamId, teamName: teams[0].name };
281
+ }
282
+ if (!opts.interactive) {
283
+ console.log(import_chalk.default.yellow(`Linking to "${teams[0].name}" \u2014 pass --team to choose another.`));
284
+ return { teamId: teams[0].teamId, teamName: teams[0].name };
285
+ }
286
+ const { default: inquirer2 } = await import("inquirer");
287
+ const { chosen } = await inquirer2.prompt([{
288
+ type: "list",
289
+ name: "chosen",
290
+ message: "Which team do you want to link to?",
291
+ choices: teams.map((t) => ({ name: t.name, value: t.teamId }))
292
+ }]);
293
+ const picked = teams.find((t) => t.teamId === chosen);
294
+ return { teamId: picked.teamId, teamName: picked.name };
295
+ }
296
+ var import_chalk;
297
+ var init_team = __esm({
298
+ "src/lib/team.ts"() {
299
+ "use strict";
300
+ import_chalk = __toESM(require("chalk"));
301
+ init_api();
302
+ }
303
+ });
304
+
235
305
  // src/lib/anon-cred.ts
236
306
  var anon_cred_exports = {};
237
307
  __export(anon_cred_exports, {
@@ -492,7 +562,7 @@ var import_commander = require("commander");
492
562
  var import_chalk34 = __toESM(require("chalk"));
493
563
 
494
564
  // package.json
495
- var version = "0.12.1";
565
+ var version = "0.14.0";
496
566
 
497
567
  // src/index.ts
498
568
  init_types();
@@ -501,43 +571,7 @@ init_types();
501
571
  var import_chalk2 = __toESM(require("chalk"));
502
572
  var import_ora = __toESM(require("ora"));
503
573
  init_auth();
504
-
505
- // src/lib/team.ts
506
- var import_chalk = __toESM(require("chalk"));
507
- init_api();
508
- async function resolveLinkedTeam(opts) {
509
- let teams;
510
- try {
511
- teams = await getTeams();
512
- } catch {
513
- return opts.preferredId ? { teamId: opts.preferredId } : null;
514
- }
515
- if (teams.length === 0) return null;
516
- const preferred = opts.preferredId ? teams.find((t) => t.teamId === opts.preferredId) : void 0;
517
- if (preferred) return { teamId: preferred.teamId, teamName: preferred.name };
518
- if (opts.preferredId) {
519
- console.log(import_chalk.default.yellow("\nYour previously linked team is no longer available."));
520
- }
521
- if (teams.length === 1) {
522
- console.log(`${import_chalk.default.cyan("\u2192")} Linking to your team ${import_chalk.default.bold(teams[0].name)}.`);
523
- return { teamId: teams[0].teamId, teamName: teams[0].name };
524
- }
525
- if (!opts.interactive) {
526
- console.log(import_chalk.default.yellow(`Linking to "${teams[0].name}" \u2014 pass --team to choose another.`));
527
- return { teamId: teams[0].teamId, teamName: teams[0].name };
528
- }
529
- const { default: inquirer2 } = await import("inquirer");
530
- const { chosen } = await inquirer2.prompt([{
531
- type: "list",
532
- name: "chosen",
533
- message: "Which team do you want to link to?",
534
- choices: teams.map((t) => ({ name: t.name, value: t.teamId }))
535
- }]);
536
- const picked = teams.find((t) => t.teamId === chosen);
537
- return { teamId: picked.teamId, teamName: picked.name };
538
- }
539
-
540
- // src/commands/login.ts
574
+ init_team();
541
575
  var DASHBOARD_BASE2 = "https://dashboard.apiblaze.com";
542
576
  function openBrowser(url) {
543
577
  const { exec } = require("child_process");
@@ -613,7 +647,12 @@ async function runLogin() {
613
647
  }
614
648
  let teamId = defaultTeamId ?? void 0;
615
649
  let teamName;
616
- const linked = await resolveLinkedTeam({ preferredId: defaultTeamId ?? void 0, interactive: !!process.stdin.isTTY });
650
+ const linked = await resolveLinkedTeam({
651
+ preferredId: defaultTeamId ?? void 0,
652
+ interactive: !!process.stdin.isTTY,
653
+ // >1 team → ASK (JWT default is just the cursor). One team → link silently.
654
+ promptWhenMultiple: true
655
+ });
617
656
  if (linked) {
618
657
  teamId = linked.teamId;
619
658
  teamName = linked.teamName;
@@ -838,6 +877,9 @@ function formatCapturedRequest(req, note) {
838
877
  ].join("\n");
839
878
  }
840
879
 
880
+ // src/commands/dev.ts
881
+ init_team();
882
+
841
883
  // src/lib/random-name.ts
842
884
  var ADJECTIVES = [
843
885
  "amber",
@@ -2858,13 +2900,28 @@ var trailingComma = /\s*,\s*/;
2858
2900
  var parseList = (s) => s.split(trailingComma).map((x) => x.trim()).filter(Boolean);
2859
2901
  async function runTenantManage(query, opts) {
2860
2902
  const { teamId } = await resolveTeam(opts.team);
2861
- const slug = opts.tenant ?? loadCredentialsTenant(query) ?? await pickTenant(teamId, { message: "Manage which tenant?", initialQuery: query, allowCreate: true });
2903
+ const slug = opts.tenant ?? await validScopedTenant(teamId, query) ?? await pickTenant(teamId, { message: "Manage which tenant?", initialQuery: query, allowCreate: true });
2862
2904
  if (!slug) return;
2863
2905
  await tenantHome(teamId, slug);
2864
2906
  }
2865
- function loadCredentialsTenant(query) {
2907
+ async function validScopedTenant(teamId, query) {
2866
2908
  if (query) return void 0;
2867
- return loadCredentials()?.activeTenant ?? void 0;
2909
+ const creds = loadCredentials();
2910
+ const scoped = creds?.activeTenant;
2911
+ if (!scoped) return void 0;
2912
+ const out = await admin({
2913
+ method: "GET",
2914
+ path: `/teams/${encodeURIComponent(teamId)}/tenants?q=${encodeURIComponent(scoped)}&limit=15`,
2915
+ summary: `Verify tenant scope ${scoped}`
2916
+ }).catch(() => null);
2917
+ const names = (out?.tenants ?? []).map((t) => typeof t === "string" ? t : t.tenant_name);
2918
+ if (names.includes(scoped)) return scoped;
2919
+ const { saveCredentials: saveCredentials2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
2920
+ const next = { ...creds };
2921
+ delete next.activeTenant;
2922
+ saveCredentials2(next);
2923
+ console.log(import_chalk24.default.yellow(`Tenant scope "${scoped}" no longer exists in this team \u2014 cleared.`));
2924
+ return void 0;
2868
2925
  }
2869
2926
  async function tenantHome(teamId, tenant2) {
2870
2927
  const { default: inquirer2 } = await import("inquirer");
@@ -5050,16 +5107,25 @@ async function opCall(call) {
5050
5107
  function printResidue(report, applied) {
5051
5108
  const up = report?.upstash ?? {};
5052
5109
  const fga = report?.fga ?? {};
5110
+ const ghosts = report?.ghosts ?? {};
5053
5111
  console.log(import_chalk33.default.bold(applied ? "\nExternal-residue sweep" : "\nExternal residue (dry-run \u2014 nothing deleted)"));
5054
5112
  console.log(import_chalk33.default.bold("\n Upstash"));
5055
5113
  const orphans = up.orphans ?? [];
5056
5114
  if (orphans.length === 0) console.log(import_chalk33.default.green(" no orphaned keys"));
5057
5115
  for (const o of orphans) console.log(` ${import_chalk33.default.yellow(o.key)} ${import_chalk33.default.dim(`\u2014 ${o.reason}`)}`);
5058
5116
  console.log(import_chalk33.default.dim(` kept (live principals): ${up.kept ?? 0} \xB7 anon wallets (untouched): ${up.anon_wallets ?? 0}`));
5117
+ if (up.anon_wallet_detail) {
5118
+ const d = up.anon_wallet_detail;
5119
+ console.log(import_chalk33.default.dim(` anon wallets: ${d.count} ($${(d.total_cents / 100).toFixed(2)}), ${d.no_ttl} with NO TTL${d.no_ttl ? " \u26A0" : " (all self-expire)"}`));
5120
+ }
5121
+ if (up.keyspace_census) {
5122
+ const census = Object.entries(up.keyspace_census).map(([k, v]) => `${k}=${v}`).join(" \xB7 ");
5123
+ console.log(import_chalk33.default.dim(` keyspace: ${census}`));
5124
+ }
5059
5125
  if (up.unknown?.length) console.log(import_chalk33.default.dim(` unknown (never deleted): ${up.unknown.join(", ")}`));
5060
5126
  if (applied) console.log(` ${import_chalk33.default.bold(String(up.deleted ?? 0))} key(s) deleted`);
5061
5127
  for (const e of up.errors ?? []) console.log(import_chalk33.default.red(` error: ${e}`));
5062
- console.log(import_chalk33.default.bold("\n OpenFGA / Neon"));
5128
+ console.log(import_chalk33.default.bold("\n OpenFGA / Neon \u2014 orphan stores"));
5063
5129
  if (applied) {
5064
5130
  const swept = fga?.swept ?? [];
5065
5131
  if (swept.length === 0) console.log(import_chalk33.default.green(" no orphaned stores"));
@@ -5069,6 +5135,8 @@ function printResidue(report, applied) {
5069
5135
  );
5070
5136
  }
5071
5137
  if (fga?.remaining) console.log(import_chalk33.default.yellow(` ${fga.remaining} more orphan store(s) \u2014 re-run to drain`));
5138
+ const st = fga?.side_tables;
5139
+ if (st) console.log(import_chalk33.default.dim(` Neon side-tables purged: ${st.soft_deleted_stores} store records, ${st.orphan_models} models, ${st.orphan_changelog} changelog rows${st.error ? ` (${st.error})` : ""}`));
5072
5140
  } else {
5073
5141
  const fgaOrphans = fga?.orphans ?? [];
5074
5142
  if (fgaOrphans.length === 0) console.log(import_chalk33.default.green(" no orphaned stores"));
@@ -5077,8 +5145,26 @@ function printResidue(report, applied) {
5077
5145
  console.log(` ${import_chalk33.default.yellow(s.store_id)} ${import_chalk33.default.dim(`\u2014 ${src}${s.name ? ` (${s.name})` : ""}, ${s.neon_tuples} Neon tuple(s)`)}`);
5078
5146
  }
5079
5147
  console.log(import_chalk33.default.dim(` kept stores: ${(fga?.kept_store_ids ?? []).length}`));
5148
+ const st = fga?.side_tables;
5149
+ if (st) console.log(import_chalk33.default.dim(` Neon side-table residue: ${st.soft_deleted_stores} soft-deleted store records, ${st.orphan_models} orphan models, ${st.orphan_changelog} orphan changelog rows`));
5080
5150
  }
5081
5151
  for (const e of fga?.errors ?? []) console.log(import_chalk33.default.red(` error: ${e}`));
5152
+ console.log(import_chalk33.default.bold("\n OpenFGA \u2014 ghost tuples in surviving stores"));
5153
+ if (applied) {
5154
+ if ((ghosts?.ghost_count ?? 0) === 0) console.log(import_chalk33.default.green(" no ghost tuples"));
5155
+ else console.log(` ${import_chalk33.default.bold(String(ghosts.deleted ?? 0))} ghost tuple(s) deleted ${import_chalk33.default.dim(`(of ${ghosts.ghost_count} found, ${ghosts.scanned_tuples} scanned across ${ghosts.live_stores} live stores)`)}`);
5156
+ } else {
5157
+ const n = ghosts?.ghost_count ?? 0;
5158
+ if (n === 0) console.log(import_chalk33.default.green(` no ghost tuples ${import_chalk33.default.dim(`(${ghosts.scanned_tuples ?? 0} scanned across ${ghosts.live_stores ?? 0} live stores)`)}`));
5159
+ else {
5160
+ console.log(import_chalk33.default.yellow(` ${n} ghost tuple(s) referencing entities absent from D1:`));
5161
+ for (const g of (ghosts.ghosts ?? []).slice(0, 20)) {
5162
+ console.log(import_chalk33.default.dim(` ${g.object_type}:${g.object_id} ${g.relation} ${g._user}`));
5163
+ }
5164
+ if (n > 20) console.log(import_chalk33.default.dim(` \u2026 and ${n - 20} more`));
5165
+ }
5166
+ }
5167
+ for (const e of ghosts?.errors ?? []) console.log(import_chalk33.default.red(` error: ${e}`));
5082
5168
  console.log();
5083
5169
  }
5084
5170
  async function runOp(sub, opts = {}) {
@@ -5096,8 +5182,9 @@ async function runOp(sub, opts = {}) {
5096
5182
  console.log(import_chalk33.default.bold("\nOperator menu"));
5097
5183
  console.log(` ${import_chalk33.default.cyan("apiblaze op residue")} external-store residue report (Upstash + Neon/OpenFGA, dry-run)`);
5098
5184
  console.log(` ${import_chalk33.default.cyan("apiblaze op sweep")} delete the orphans the report shows (asks first; ${import_chalk33.default.dim("-y to skip")})`);
5099
- console.log(` ${import_chalk33.default.cyan("apiblaze op credits")} list credit wallets
5100
- `);
5185
+ console.log(` ${import_chalk33.default.cyan("apiblaze op credits")} list credit wallets`);
5186
+ console.log(import_chalk33.default.dim(` (to prune all non-CP data: run scripts/nuke-but-cp.sh --apply --sweep in the repo)
5187
+ `));
5101
5188
  return;
5102
5189
  }
5103
5190
  case "residue": {
@@ -5110,15 +5197,20 @@ async function runOp(sub, opts = {}) {
5110
5197
  const report = await opCall({ method: "GET", path: "/operator/external-residue", summary: "external residue report" });
5111
5198
  const nUp = report?.upstash?.orphans?.length ?? 0;
5112
5199
  const nFga = report?.fga?.orphans?.length ?? 0;
5200
+ const nGhost = report?.ghosts?.ghost_count ?? 0;
5201
+ const st = report?.fga?.side_tables ?? {};
5202
+ const nSide = (st.soft_deleted_stores ?? 0) + (st.orphan_models ?? 0) + (st.orphan_changelog ?? 0);
5113
5203
  printResidue(report, false);
5114
- if (nUp + nFga === 0) {
5204
+ if (nUp + nFga + nGhost + nSide === 0) {
5115
5205
  console.log(import_chalk33.default.green("Nothing to sweep."));
5116
5206
  return;
5117
5207
  }
5118
5208
  if (!opts.yes) {
5119
5209
  const readline2 = await import("readline/promises");
5120
5210
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
5121
- const answer = await rl.question(import_chalk33.default.red(`Delete ${nUp} Upstash key(s) + ${nFga} OpenFGA store(s)? Type 'sweep' to confirm: `));
5211
+ const answer = await rl.question(
5212
+ import_chalk33.default.red(`Delete ${nUp} Upstash key(s) + ${nFga} OpenFGA store(s) + ${nGhost} ghost tuple(s) + ${nSide} Neon side-table row(s)? Type 'sweep' to confirm: `)
5213
+ );
5122
5214
  rl.close();
5123
5215
  if (answer.trim() !== "sweep") return void console.log(import_chalk33.default.dim("Aborted."));
5124
5216
  }
@@ -5145,6 +5237,7 @@ async function runOp(sub, opts = {}) {
5145
5237
 
5146
5238
  // src/index.ts
5147
5239
  init_trace();
5240
+ init_auth();
5148
5241
  var program = new import_commander.Command();
5149
5242
  program.name("apiblaze").description("APIblaze CLI \u2014 create & manage API proxies and run dev tunnels").version(version).option("-v, --verbose", "Print the exact series of API calls each command makes (curl-equivalent you could run yourself)");
5150
5243
  program.hook("preAction", () => {
@@ -5158,7 +5251,7 @@ function action(fn) {
5158
5251
  renderTrace();
5159
5252
  } catch (err) {
5160
5253
  renderTrace();
5161
- printError(err);
5254
+ await printError(err);
5162
5255
  process.exit(1);
5163
5256
  }
5164
5257
  };
@@ -5167,7 +5260,7 @@ program.command("login").description("Authenticate with APIblaze").action(async
5167
5260
  try {
5168
5261
  await runLogin();
5169
5262
  } catch (err) {
5170
- printError(err);
5263
+ await printError(err);
5171
5264
  process.exit(1);
5172
5265
  }
5173
5266
  });
@@ -5175,7 +5268,7 @@ program.command("create").description("Create a new API proxy (no login needed \
5175
5268
  try {
5176
5269
  await runCreate(opts);
5177
5270
  } catch (err) {
5178
- printError(err);
5271
+ await printError(err);
5179
5272
  process.exit(1);
5180
5273
  }
5181
5274
  });
@@ -5200,7 +5293,7 @@ program.command("dev").description("Put your localhost behind a public URL (dev
5200
5293
  }
5201
5294
  await runDev({ port: resolved, captureFile: opts.captureFile });
5202
5295
  } catch (err) {
5203
- printError(err);
5296
+ await printError(err);
5204
5297
  process.exit(1);
5205
5298
  }
5206
5299
  });
@@ -5213,7 +5306,7 @@ program.command("whoami").description("Show who you are \u2014 both API Producer
5213
5306
  try {
5214
5307
  await runWhoami(opts);
5215
5308
  } catch (err) {
5216
- printError(err);
5309
+ await printError(err);
5217
5310
  process.exit(1);
5218
5311
  }
5219
5312
  });
@@ -5221,7 +5314,7 @@ program.command("claim").description("Claim your anonymous workspace into your a
5221
5314
  try {
5222
5315
  await runClaim(code, opts);
5223
5316
  } catch (err) {
5224
- printError(err);
5317
+ await printError(err);
5225
5318
  process.exit(1);
5226
5319
  }
5227
5320
  });
@@ -5229,7 +5322,7 @@ program.command("team").description("Switch the active team").argument("[team]",
5229
5322
  try {
5230
5323
  await runTeam(team);
5231
5324
  } catch (err) {
5232
- printError(err);
5325
+ await printError(err);
5233
5326
  process.exit(1);
5234
5327
  }
5235
5328
  });
@@ -5237,7 +5330,7 @@ program.command("projects").description("List the projects in your team").action
5237
5330
  try {
5238
5331
  await runProjects();
5239
5332
  } catch (err) {
5240
- printError(err);
5333
+ await printError(err);
5241
5334
  process.exit(1);
5242
5335
  }
5243
5336
  });
@@ -5319,14 +5412,32 @@ Examples:
5319
5412
  $ npx apiblaze throttle myapi --rate 50 --verbose # configure + show the API call
5320
5413
  $ npx apiblaze consumer login # act as a consumer of your API
5321
5414
  `);
5322
- function printError(err) {
5415
+ async function recoverStaleTeam() {
5416
+ try {
5417
+ const creds = loadCredentials();
5418
+ if (!creds?.teamId) return;
5419
+ const { resolveLinkedTeam: resolveLinkedTeam2 } = await Promise.resolve().then(() => (init_team(), team_exports));
5420
+ const linked = await resolveLinkedTeam2({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
5421
+ if (!linked) {
5422
+ console.error(import_chalk34.default.yellow("Your account has no teams anymore (deleted?). Run `apiblaze login` or `apiblaze create` to get a workspace."));
5423
+ return;
5424
+ }
5425
+ if (linked.teamId === creds.teamId) return;
5426
+ const next = { ...creds, teamId: linked.teamId, teamName: linked.teamName };
5427
+ delete next.activeTenant;
5428
+ saveCredentials(next);
5429
+ console.error(import_chalk34.default.yellow(`Your previous team no longer exists \u2014 relinked to ${import_chalk34.default.bold(linked.teamName ?? linked.teamId)}. Re-run your command.`));
5430
+ } catch {
5431
+ }
5432
+ }
5433
+ async function printError(err) {
5323
5434
  if (err instanceof ApiError) {
5324
5435
  const data = err.body;
5325
5436
  const extra = [data?.body?.reason, data?.body?.details, data?.details, data?.body?.error].find((x) => typeof x === "string" && x && x !== err.message);
5326
5437
  console.error(import_chalk34.default.red(`
5327
5438
  API error (${err.status}): ${err.message}${extra ? ` \u2014 ${extra}` : ""}`));
5328
5439
  if (err.status === 403 || err.status === 404) {
5329
- console.error(import_chalk34.default.dim("If your team/tenant was recently deleted or recreated, re-run `apiblaze login` or switch with `apiblaze team`."));
5440
+ await recoverStaleTeam();
5330
5441
  }
5331
5442
  } else if (err instanceof Error) {
5332
5443
  console.error(import_chalk34.default.red(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "APIblaze CLI + sidecar — manage API proxies, run dev tunnels, and route a Next.js app's egress through APIblaze with one command",
5
5
  "keywords": [
6
6
  "apiblaze",