apiblaze 0.12.1 → 0.15.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 +915 -762
  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, {
@@ -300,180 +370,545 @@ var init_anon_cred = __esm({
300
370
  }
301
371
  });
302
372
 
303
- // src/lib/trace.ts
304
- function setVerbose(v) {
305
- verbose = v;
306
- }
307
- function recordCall(e) {
308
- if (verbose) entries.push(e);
309
- }
310
- function maskBody(body) {
311
- if (body === void 0) return void 0;
312
- return JSON.stringify(body, (k, v) => SECRET_KEY.test(k) && typeof v === "string" ? "***" : v);
313
- }
314
- function renderTrace() {
315
- if (!verbose || entries.length === 0) return;
316
- console.log(import_chalk15.default.dim("\n" + "\u2500".repeat(64)));
317
- console.log(import_chalk15.default.bold(`--verbose: ${entries.length} API call${entries.length === 1 ? "" : "s"} this command made`));
318
- console.log(
319
- import_chalk15.default.dim("The same thing on the official API \u2014 copy/paste with your control-plane key\n(get one from the Developers section of dashboard.apiblaze.com, then\n`export APIBLAZE_CONTROLPLANE_APIKEY=sk_...`).\nFull API reference: https://api.apiblaze.com/openapi.json\n")
320
- );
321
- entries.forEach((e, i) => {
322
- const n = entries.length > 1 ? import_chalk15.default.bold(`${i + 1}. `) : "";
323
- if (e.summary) console.log(`${n}${import_chalk15.default.cyan(e.summary)}${e.status ? import_chalk15.default.dim(` (HTTP ${e.status})`) : ""}`);
324
- const url = `https://api.apiblaze.com/${CONTROL_API_VERSION}/prod${e.path}`;
325
- const masked = maskBody(e.body);
326
- const hasBody = e.method !== "GET" && masked !== void 0;
327
- console.log(import_chalk15.default.green(` curl -sS -X ${e.method} ${url}` + (hasBody ? " \\" : "")));
328
- console.log(import_chalk15.default.green(' -H "X-API-Key: $APIBLAZE_CONTROLPLANE_APIKEY"' + (hasBody ? " \\" : "")));
329
- if (hasBody) {
330
- console.log(import_chalk15.default.green(" -H 'Content-Type: application/json' \\"));
331
- console.log(import_chalk15.default.green(` -d '${masked}'`));
332
- }
333
- if (i < entries.length - 1) console.log();
334
- });
335
- entries.length = 0;
336
- }
337
- var import_chalk15, CONTROL_API_VERSION, verbose, entries, SECRET_KEY;
338
- var init_trace = __esm({
339
- "src/lib/trace.ts"() {
340
- "use strict";
341
- import_chalk15 = __toESM(require("chalk"));
342
- CONTROL_API_VERSION = "1.0.0";
343
- verbose = false;
344
- entries = [];
345
- SECRET_KEY = /secret|token|password|api[_-]?key|client_secret/i;
346
- }
373
+ // src/commands/create.ts
374
+ var create_exports = {};
375
+ __export(create_exports, {
376
+ buildTryItCurl: () => buildTryItCurl,
377
+ runCreate: () => runCreate
347
378
  });
348
-
349
- // src/lib/admin.ts
350
- async function admin(call) {
351
- const token = getAccessToken();
352
- const res = await fetch(`${DASHBOARD_BASE3}/api/cli/admin`, {
353
- method: "POST",
354
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
355
- body: JSON.stringify({ path: call.path, method: call.method, body: call.body })
356
- });
357
- let data = null;
379
+ function normalizeName(raw) {
380
+ return (raw || "").toLowerCase().replace(/[^a-z0-9]/g, "");
381
+ }
382
+ function isHttpUrl(s) {
358
383
  try {
359
- data = await res.json();
384
+ const u = new URL((s || "").trim());
385
+ return u.protocol === "http:" || u.protocol === "https:";
360
386
  } catch {
387
+ return false;
361
388
  }
362
- recordCall({ method: call.method, path: call.path, body: call.body, status: res.status, summary: call.summary });
363
- maybePrintBilling(data);
364
- if (!res.ok) {
365
- const msg = data?.details ?? data?.error ?? res.statusText;
366
- throw new ApiError(res.status, typeof msg === "string" ? msg : JSON.stringify(msg), data);
367
- }
368
- return data;
369
389
  }
370
- function maybePrintBilling(data) {
371
- const b = data?.billing;
372
- if (b && typeof b.charged_cents === "number") {
373
- const usd = (b.charged_cents / 100).toFixed(2);
374
- const rem = typeof b.credits_remaining === "number" ? ` \xB7 $${(b.credits_remaining / 100).toFixed(2)} credit left` : "";
375
- console.log(import_chalk16.default.magenta(` \u{1F4B3} Charged $${usd}${rem}`));
390
+ function stripTenantFromPortal(devPortal) {
391
+ try {
392
+ const u = new URL(devPortal);
393
+ const dot = u.hostname.indexOf(".");
394
+ if (dot < 0) return devPortal;
395
+ const product = u.hostname.slice(0, dot).split("-")[0];
396
+ u.hostname = `${product}${u.hostname.slice(dot)}`;
397
+ return u.toString();
398
+ } catch {
399
+ return devPortal;
376
400
  }
377
401
  }
378
- var import_chalk16, DASHBOARD_BASE3;
379
- var init_admin = __esm({
380
- "src/lib/admin.ts"() {
381
- "use strict";
382
- import_chalk16 = __toESM(require("chalk"));
383
- init_auth();
384
- init_trace();
385
- init_types();
386
- DASHBOARD_BASE3 = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
402
+ function fail(message) {
403
+ console.error(import_chalk6.default.red(`Error: ${message}`));
404
+ process.exit(1);
405
+ }
406
+ function buildTryItCurl(url, authType, apiKey) {
407
+ if (authType === "api_key") {
408
+ if (!apiKey) return null;
409
+ return `curl ${url} -H "X-API-Key: ${apiKey}"`;
387
410
  }
388
- });
389
-
390
- // src/lib/tenant-pick.ts
391
- var tenant_pick_exports = {};
392
- __export(tenant_pick_exports, {
393
- pickTenant: () => pickTenant
394
- });
395
- async function fetchPage(teamId, q) {
396
- const out = await admin({
397
- method: "GET",
398
- path: `/teams/${encodeURIComponent(teamId)}/tenants?detail=1&limit=${PAGE}${q ? `&q=${encodeURIComponent(q)}` : ""}`,
399
- summary: q ? `Search tenants matching "${q}"` : "List tenants (first page)"
400
- });
401
- const rows = (out?.tenants ?? []).map(
402
- (t) => typeof t === "string" ? { tenant_name: t } : t
403
- );
404
- return { rows, total: out?.total ?? rows.length, defaultTenant: out?.default_tenant ?? null };
411
+ if (authType === "none") return `curl ${url}`;
412
+ return null;
405
413
  }
406
- function label(t, defaultTenant, active) {
407
- const tags = [
408
- t.tenant_name === active ? import_chalk22.default.cyan("active scope") : "",
409
- t.tenant_name === defaultTenant ? import_chalk22.default.dim("team default") : ""
410
- ].filter(Boolean).join(", ");
411
- const disp = t.display_name && t.display_name !== t.tenant_name ? import_chalk22.default.dim(` ${t.display_name}`) : "";
412
- return `${t.tenant_name}${disp}${tags ? ` (${tags})` : ""}`;
414
+ function printCurlExample(url, authType, apiKey, devPortal) {
415
+ const curl = buildTryItCurl(url, authType, apiKey);
416
+ console.log();
417
+ if (curl) {
418
+ console.log(` ${import_chalk6.default.dim("Try it \u2014 copy/paste:")}`);
419
+ console.log(` ${import_chalk6.default.cyan(curl)}`);
420
+ } else if (authType === "oauth") {
421
+ console.log(` ${import_chalk6.default.dim("Try it:")} this proxy uses OAuth \u2014 sign in at ${import_chalk6.default.bold(devPortal ?? "the dev portal")} to get a token,`);
422
+ console.log(` ${import_chalk6.default.dim(`then call ${url} with`)} ${import_chalk6.default.cyan('-H "Authorization: Bearer <token>"')}`);
423
+ }
413
424
  }
414
- async function pickTenant(teamId, opts = {}) {
415
- const { default: inquirer2 } = await import("inquirer");
416
- const active = loadCredentials()?.activeTenant;
417
- let q = opts.initialQuery ?? "";
418
- for (; ; ) {
419
- const spinner = (0, import_ora8.default)(q ? `Searching tenants for "${q}"...` : "Loading tenants...").start();
420
- const page = await fetchPage(teamId, q).finally(() => spinner.stop());
421
- if (!page.total && !q) {
422
- if (opts.allowCreate) {
423
- const { make } = await inquirer2.prompt([{ type: "confirm", name: "make", message: "No tenants yet \u2014 create one?", default: true }]);
424
- if (make) return await createTenantInline(teamId);
425
+ async function runCreate(opts = {}) {
426
+ const creds = loadCredentials();
427
+ if (!creds) {
428
+ await runAnonymousCreate(opts);
429
+ return;
430
+ }
431
+ const interactive = !!process.stdin.isTTY && !opts.json;
432
+ const auth = (opts.auth ?? "api_key").toLowerCase();
433
+ if (!VALID_AUTH.includes(auth)) {
434
+ fail(`Invalid --auth "${auth}". Use one of: ${VALID_AUTH.join(", ")}.`);
435
+ }
436
+ let teamId = creds.teamId;
437
+ if (opts.team) {
438
+ if (opts.team.startsWith("team_")) {
439
+ teamId = opts.team;
440
+ } else {
441
+ const teams = await getTeams().catch(() => []);
442
+ const match = teams.find(
443
+ (t) => t.teamId === opts.team || t.name.toLowerCase() === opts.team.toLowerCase()
444
+ );
445
+ if (!match) {
446
+ fail(`Team "${opts.team}" not found. Run \`apiblaze team\` to see your teams.`);
425
447
  }
426
- console.error(import_chalk22.default.red("This team has no tenants. Create one with `apiblaze tenant create`."));
427
- return null;
428
- }
429
- const truncated = page.total > page.rows.length;
430
- const choices = page.rows.map((t) => ({
431
- name: label(t, page.defaultTenant, active),
432
- value: t.tenant_name
433
- }));
434
- if (truncated || q) {
435
- choices.push(new inquirer2.Separator(import_chalk22.default.dim(
436
- truncated ? `showing ${page.rows.length} of ${page.total}${q ? ` matching "${q}"` : ""} \u2014 search to narrow` : `matches for "${q}"`
437
- )));
438
- choices.push({ name: `\u{1F50D} Search${q ? " again" : ""}\u2026`, value: "\0search" });
439
- }
440
- if (q) choices.push({ name: "\u21BA Show all (clear search)", value: "\0clear" });
441
- if (opts.allowCreate) choices.push({ name: "\uFF0B Create a new tenant\u2026", value: "\0create" });
442
- if (opts.allowBack) choices.push({ name: "\u2190 Back", value: "\0back" });
443
- const { picked } = await inquirer2.prompt([{
444
- type: "list",
445
- name: "picked",
446
- pageSize: PAGE + 5,
447
- message: opts.message ?? "Which tenant?",
448
- default: active && page.rows.some((t) => t.tenant_name === active) ? active : void 0,
449
- choices
450
- }]);
451
- if (picked === "\0back") return null;
452
- if (picked === "\0clear") {
453
- q = "";
454
- continue;
455
- }
456
- if (picked === "\0create") return await createTenantInline(teamId);
457
- if (picked === "\0search") {
458
- const { nq } = await inquirer2.prompt([{ type: "input", name: "nq", message: "Search (name or display name):", default: q }]);
459
- q = String(nq ?? "").trim();
460
- continue;
448
+ teamId = match.teamId;
461
449
  }
462
- return picked;
463
450
  }
464
- }
465
- async function createTenantInline(teamId) {
466
- const { default: inquirer2 } = await import("inquirer");
467
- const { display } = await inquirer2.prompt([{ type: "input", name: "display", message: "Display name for the new tenant:", validate: (s) => !!s.trim() || "required" }]);
468
- const out = await admin({
451
+ if (!opts.json) console.log(import_chalk6.default.bold("\nCreate an API proxy\n"));
452
+ let name = "";
453
+ if (opts.name !== void 0) {
454
+ name = normalizeName(opts.name);
455
+ if (name.length < 3) fail("Proxy name must be at least 3 characters (letters and digits only).");
456
+ const check = await checkProxyName(name, teamId, opts.apiversion).catch(() => null);
457
+ if (check && (!check.canUseProjectName || !check.canUseApiVersion)) {
458
+ fail(`Proxy name "${name}" is not available${check.message ? ` \u2014 ${check.message}` : ""}.`);
459
+ }
460
+ } else if (interactive) {
461
+ const { default: inquirer2 } = await import("inquirer");
462
+ for (; ; ) {
463
+ const { rawName } = await inquirer2.prompt([{
464
+ type: "input",
465
+ name: "rawName",
466
+ message: "Proxy name (your API will live at <name>.abz.run):",
467
+ transformer: (v) => normalizeName(v)
468
+ }]);
469
+ name = normalizeName(rawName);
470
+ if (name.length < 3) {
471
+ console.log(import_chalk6.default.yellow(" Name must be at least 3 characters (letters and digits only).\n"));
472
+ continue;
473
+ }
474
+ const spinner2 = (0, import_ora4.default)("Checking availability...").start();
475
+ try {
476
+ const check = await checkProxyName(name, teamId, opts.apiversion);
477
+ spinner2.stop();
478
+ if (!check.canUseProjectName || !check.canUseApiVersion) {
479
+ console.log(import_chalk6.default.yellow(` "${name}" is not available${check.message ? ` \u2014 ${check.message}` : ""}. Try another.
480
+ `));
481
+ continue;
482
+ }
483
+ } catch {
484
+ spinner2.stop();
485
+ console.log(import_chalk6.default.dim(" (could not verify availability; continuing)"));
486
+ }
487
+ console.log(`${import_chalk6.default.cyan("\u2192")} Your API will live at ${import_chalk6.default.bold(`https://${name}.abz.run`)}
488
+ `);
489
+ break;
490
+ }
491
+ } else {
492
+ fail("--name is required in non-interactive mode.");
493
+ }
494
+ let targetUrl = "";
495
+ if (opts.target !== void 0) {
496
+ if (!isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
497
+ targetUrl = opts.target.trim();
498
+ } else if (interactive) {
499
+ const { default: inquirer2 } = await import("inquirer");
500
+ for (; ; ) {
501
+ const { url } = await inquirer2.prompt([{
502
+ type: "input",
503
+ name: "url",
504
+ message: "Target URL to forward requests to (e.g. https://httpbin.org):"
505
+ }]);
506
+ if (!isHttpUrl(url)) {
507
+ console.log(import_chalk6.default.yellow(" Enter a valid http(s) URL.\n"));
508
+ continue;
509
+ }
510
+ targetUrl = url.trim();
511
+ break;
512
+ }
513
+ } else {
514
+ fail("--target is required in non-interactive mode.");
515
+ }
516
+ if (interactive && !opts.yes) {
517
+ const { default: inquirer2 } = await import("inquirer");
518
+ console.log(`${import_chalk6.default.cyan("\u2192")} Auth: ${import_chalk6.default.bold(auth)}${auth === "api_key" ? " \u2014 consumers send an X-API-Key header" : ""}`);
519
+ const { ok } = await inquirer2.prompt([{
520
+ type: "confirm",
521
+ name: "ok",
522
+ message: `Create proxy "${name}" \u2192 ${targetUrl}?`,
523
+ default: true
524
+ }]);
525
+ if (!ok) {
526
+ console.log(import_chalk6.default.yellow("Cancelled."));
527
+ return;
528
+ }
529
+ }
530
+ const spinner = !opts.json ? (0, import_ora4.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
531
+ let result;
532
+ try {
533
+ result = await createProxy({ name, target_url: targetUrl, auth_type: auth, team_id: teamId, ...opts.apiversion ? { api_version: opts.apiversion } : {} });
534
+ spinner?.succeed(import_chalk6.default.green("Proxy created!"));
535
+ } catch (err) {
536
+ spinner?.fail("Failed to create proxy.");
537
+ throw err;
538
+ }
539
+ const version2 = result.api_version || "1.0.0";
540
+ const keys = result.api_keys ?? {};
541
+ const adminKey = keys.dev ?? Object.values(keys)[0];
542
+ const proxyUrl = `https://${name}.abz.run/${version2}/dev`;
543
+ const devPortal = result.devPortal ? stripTenantFromPortal(result.devPortal) : void 0;
544
+ if (opts.json) {
545
+ process.stdout.write(JSON.stringify({
546
+ project_id: result.project_id,
547
+ api_version: version2,
548
+ proxy_url: proxyUrl,
549
+ dev_portal: devPortal,
550
+ api_key: adminKey,
551
+ api_keys: keys,
552
+ team_id: teamId
553
+ }) + "\n");
554
+ return;
555
+ }
556
+ console.log();
557
+ console.log(` ${import_chalk6.default.dim("Proxy URL: ")} ${import_chalk6.default.bold(proxyUrl)}`);
558
+ if (devPortal) console.log(` ${import_chalk6.default.dim("Dev portal:")} ${import_chalk6.default.bold(devPortal)}`);
559
+ if (adminKey) {
560
+ console.log();
561
+ console.log(` ${import_chalk6.default.dim("Consumer admin API key (dev):")}`);
562
+ console.log(` ${import_chalk6.default.bold.green(adminKey)}`);
563
+ console.log(import_chalk6.default.dim("\n Save this now \u2014 send it as the X-API-Key header. It may not be shown again."));
564
+ const otherEnvs = Object.keys(keys).filter((e) => e !== "dev");
565
+ if (otherEnvs.length) {
566
+ console.log(import_chalk6.default.dim(` (Separate keys were also created for: ${otherEnvs.join(", ")}.)`));
567
+ }
568
+ }
569
+ printCurlExample(proxyUrl, auth, adminKey, devPortal);
570
+ console.log();
571
+ }
572
+ async function runAnonymousCreate(opts) {
573
+ const interactive = !!process.stdin.isTTY && !opts.json;
574
+ if (!opts.json) {
575
+ console.log(import_chalk6.default.bold("\nCreate an API proxy"));
576
+ console.log(import_chalk6.default.dim("Not logged in \u2014 creating an anonymous proxy. You can claim it to your account within 30 days.\n"));
577
+ }
578
+ let body = {};
579
+ if (opts.config) {
580
+ let raw = "";
581
+ try {
582
+ raw = import_fs2.default.readFileSync(opts.config, "utf8");
583
+ } catch {
584
+ fail(`Cannot read --config file: ${opts.config}`);
585
+ }
586
+ let parsed;
587
+ try {
588
+ parsed = JSON.parse(raw);
589
+ } catch {
590
+ fail(`--config is not valid JSON: ${opts.config}`);
591
+ }
592
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) fail("--config must be a JSON object.");
593
+ body = parsed;
594
+ }
595
+ let name = opts.name !== void 0 ? normalizeName(opts.name) : typeof body.name === "string" ? body.name : void 0;
596
+ if (opts.name !== void 0 && name.length < 3) {
597
+ fail("Proxy name must be at least 3 characters (letters and digits only).");
598
+ }
599
+ if (opts.target && !isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
600
+ let target = opts.target?.trim() || (typeof body.target === "string" ? body.target : "") || (typeof body.target_url === "string" ? body.target_url : "");
601
+ const hasOtherSource = !!(body.openapi || body.github);
602
+ if (!target && !hasOtherSource) {
603
+ if (interactive) {
604
+ const { default: inquirer2 } = await import("inquirer");
605
+ if (name === void 0) {
606
+ const { rawName } = await inquirer2.prompt([{
607
+ type: "input",
608
+ name: "rawName",
609
+ message: "Proxy name (leave blank to auto-generate):",
610
+ transformer: (v) => normalizeName(v)
611
+ }]);
612
+ const n = normalizeName(rawName);
613
+ name = n.length >= 3 ? n : void 0;
614
+ }
615
+ for (; ; ) {
616
+ const { url } = await inquirer2.prompt([{
617
+ type: "input",
618
+ name: "url",
619
+ message: "Target URL to forward requests to (e.g. https://httpbin.org):"
620
+ }]);
621
+ if (!isHttpUrl(url)) {
622
+ console.log(import_chalk6.default.yellow(" Enter a valid http(s) URL.\n"));
623
+ continue;
624
+ }
625
+ target = url.trim();
626
+ break;
627
+ }
628
+ } else {
629
+ fail("A source is required: pass --target, or target/openapi/github in --config.");
630
+ }
631
+ }
632
+ if (target) {
633
+ body.target = target;
634
+ body.target_url = target;
635
+ }
636
+ if (name) {
637
+ body.name = name;
638
+ body.subdomain = name;
639
+ }
640
+ if (opts.subdomain) body.subdomain = normalizeName(opts.subdomain);
641
+ if (opts.tenant) body.tenant = normalizeName(opts.tenant);
642
+ if (opts.product) body.product_slug = normalizeName(opts.product);
643
+ if (opts.displayName) body.display_name = opts.displayName;
644
+ if (opts.apiversion) body.api_version = opts.apiversion;
645
+ if (opts.auth && opts.auth !== "api_key") body.auth_type = opts.auth;
646
+ const { loadAnonCred: loadAnonCred2, saveAnonCred: saveAnonCred2, clearAnonCred: clearAnonCred2, cpFetch: cpFetch2 } = await Promise.resolve().then(() => (init_anon_cred(), anon_cred_exports));
647
+ if (opts.newSession) clearAnonCred2();
648
+ const cred = loadAnonCred2();
649
+ const spinner = !opts.json ? (0, import_ora4.default)(cred ? "Creating proxy (in your anonymous workspace)..." : "Creating proxy...").start() : null;
650
+ let result;
651
+ try {
652
+ if (cred) {
653
+ result = await cpFetch2(cred.cp_key, "/projects", { method: "POST", body: JSON.stringify(body) });
654
+ if (Array.isArray(result.endpoints)) {
655
+ result.endpoints = result.endpoints.map((e) => e.replace(/:\/\/[^/]+/, `://${result.project_id}.tryabz.run`));
656
+ }
657
+ } else {
658
+ result = await createProxyAnonymous(body);
659
+ if (result.cp_key && result.team_id) {
660
+ saveAnonCred2(result.cp_key, result.team_id, result.claim_code);
661
+ }
662
+ }
663
+ spinner?.succeed(import_chalk6.default.green("Proxy created!"));
664
+ } catch (err) {
665
+ spinner?.fail("Failed to create proxy.");
666
+ throw err;
667
+ }
668
+ const version2 = result.api_version || "1.0.0";
669
+ const keys = result.api_keys ?? {};
670
+ const apiKey = result.apiKey ?? keys.prod ?? Object.values(keys)[0];
671
+ const prodEndpoint = (result.endpoints || []).find((e) => e.endsWith("/prod")) || (result.endpoints || [])[0];
672
+ if (opts.json) {
673
+ process.stdout.write(JSON.stringify({
674
+ project_id: result.project_id,
675
+ api_version: version2,
676
+ endpoints: result.endpoints,
677
+ api_key: apiKey,
678
+ api_keys: keys,
679
+ claim_url: result.claim_url,
680
+ anonymous: true
681
+ }) + "\n");
682
+ return;
683
+ }
684
+ console.log();
685
+ if (prodEndpoint) console.log(` ${import_chalk6.default.dim("Proxy URL: ")} ${import_chalk6.default.bold(prodEndpoint)}`);
686
+ if (result.portal) console.log(` ${import_chalk6.default.dim("Dev portal:")} ${import_chalk6.default.bold(result.portal)}`);
687
+ if (apiKey) {
688
+ console.log();
689
+ console.log(` ${import_chalk6.default.dim("API key:")}`);
690
+ console.log(` ${import_chalk6.default.bold.green(apiKey)}`);
691
+ console.log(import_chalk6.default.dim("\n Save this now \u2014 send it as the X-API-Key header. It may not be shown again."));
692
+ }
693
+ if (prodEndpoint) printCurlExample(prodEndpoint, opts.auth || "api_key", apiKey, result.portal);
694
+ const claimCode = result.claim_code || cred?.claim_code;
695
+ if (claimCode) {
696
+ console.log();
697
+ console.log(` ${import_chalk6.default.yellow("\u26A0 Anonymous \u2014 claim within 30 days or it expires.")} Everything you create`);
698
+ console.log(` with the CP key shares ONE workspace. To keep it all in one shot:`);
699
+ console.log(` ${import_chalk6.default.cyan("apiblaze login")} ${import_chalk6.default.dim("(prompts to claim your workspace into your account)")}`);
700
+ console.log(import_chalk6.default.dim(` From another machine: apiblaze claim ${claimCode} (add --team <name> to merge into an existing team)`));
701
+ } else if (result.claim_url) {
702
+ console.log();
703
+ console.log(` ${import_chalk6.default.yellow("\u26A0 Anonymous proxy \u2014 claim it to your account within 30 days or it expires:")}`);
704
+ console.log(` ${import_chalk6.default.bold(result.claim_url)}`);
705
+ }
706
+ console.log();
707
+ }
708
+ var import_fs2, import_chalk6, import_ora4, VALID_AUTH;
709
+ var init_create = __esm({
710
+ "src/commands/create.ts"() {
711
+ "use strict";
712
+ import_fs2 = __toESM(require("fs"));
713
+ import_chalk6 = __toESM(require("chalk"));
714
+ import_ora4 = __toESM(require("ora"));
715
+ init_auth();
716
+ init_api();
717
+ VALID_AUTH = ["api_key", "none", "oauth"];
718
+ }
719
+ });
720
+
721
+ // src/lib/trace.ts
722
+ function setVerbose(v) {
723
+ verbose = v;
724
+ }
725
+ function recordCall(e) {
726
+ if (verbose) entries.push(e);
727
+ }
728
+ function maskBody(body) {
729
+ if (body === void 0) return void 0;
730
+ return JSON.stringify(body, (k, v) => SECRET_KEY.test(k) && typeof v === "string" ? "***" : v);
731
+ }
732
+ function renderTrace() {
733
+ if (!verbose || entries.length === 0) return;
734
+ console.log(import_chalk15.default.dim("\n" + "\u2500".repeat(64)));
735
+ console.log(import_chalk15.default.bold(`--verbose: ${entries.length} API call${entries.length === 1 ? "" : "s"} this command made`));
736
+ console.log(
737
+ import_chalk15.default.dim("The same thing on the official API \u2014 copy/paste with your control-plane key\n(get one from the Developers section of dashboard.apiblaze.com, then\n`export APIBLAZE_CONTROLPLANE_APIKEY=sk_...`).\nFull API reference: https://api.apiblaze.com/openapi.json\n")
738
+ );
739
+ entries.forEach((e, i) => {
740
+ const n = entries.length > 1 ? import_chalk15.default.bold(`${i + 1}. `) : "";
741
+ if (e.summary) console.log(`${n}${import_chalk15.default.cyan(e.summary)}${e.status ? import_chalk15.default.dim(` (HTTP ${e.status})`) : ""}`);
742
+ const url = `https://api.apiblaze.com/${CONTROL_API_VERSION}/prod${e.path}`;
743
+ const masked = maskBody(e.body);
744
+ const hasBody = e.method !== "GET" && masked !== void 0;
745
+ console.log(import_chalk15.default.green(` curl -sS -X ${e.method} ${url}` + (hasBody ? " \\" : "")));
746
+ console.log(import_chalk15.default.green(' -H "X-API-Key: $APIBLAZE_CONTROLPLANE_APIKEY"' + (hasBody ? " \\" : "")));
747
+ if (hasBody) {
748
+ console.log(import_chalk15.default.green(" -H 'Content-Type: application/json' \\"));
749
+ console.log(import_chalk15.default.green(` -d '${masked}'`));
750
+ }
751
+ if (i < entries.length - 1) console.log();
752
+ });
753
+ entries.length = 0;
754
+ }
755
+ var import_chalk15, CONTROL_API_VERSION, verbose, entries, SECRET_KEY;
756
+ var init_trace = __esm({
757
+ "src/lib/trace.ts"() {
758
+ "use strict";
759
+ import_chalk15 = __toESM(require("chalk"));
760
+ CONTROL_API_VERSION = "1.0.0";
761
+ verbose = false;
762
+ entries = [];
763
+ SECRET_KEY = /secret|token|password|api[_-]?key|client_secret/i;
764
+ }
765
+ });
766
+
767
+ // src/lib/admin.ts
768
+ async function admin(call) {
769
+ const token = getAccessToken();
770
+ const res = await fetch(`${DASHBOARD_BASE3}/api/cli/admin`, {
469
771
  method: "POST",
470
- path: `/teams/${encodeURIComponent(teamId)}/tenants`,
471
- body: { display_name: display.trim() },
472
- summary: `Create tenant "${display.trim()}"`
772
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
773
+ body: JSON.stringify({ path: call.path, method: call.method, body: call.body })
473
774
  });
474
- const slug = out?.tenant_name ?? out?.tenant?.tenant_name;
475
- console.log(import_chalk22.default.green(` Tenant ${import_chalk22.default.bold(slug)} created.`));
476
- return slug;
775
+ let data = null;
776
+ try {
777
+ data = await res.json();
778
+ } catch {
779
+ }
780
+ recordCall({ method: call.method, path: call.path, body: call.body, status: res.status, summary: call.summary });
781
+ maybePrintBilling(data);
782
+ if (!res.ok) {
783
+ const msg = data?.details ?? data?.error ?? res.statusText;
784
+ throw new ApiError(res.status, typeof msg === "string" ? msg : JSON.stringify(msg), data);
785
+ }
786
+ return data;
787
+ }
788
+ function maybePrintBilling(data) {
789
+ const b = data?.billing;
790
+ if (b && typeof b.charged_cents === "number") {
791
+ const usd = (b.charged_cents / 100).toFixed(2);
792
+ const rem = typeof b.credits_remaining === "number" ? ` \xB7 $${(b.credits_remaining / 100).toFixed(2)} credit left` : "";
793
+ console.log(import_chalk16.default.magenta(` \u{1F4B3} Charged $${usd}${rem}`));
794
+ }
795
+ }
796
+ var import_chalk16, DASHBOARD_BASE3;
797
+ var init_admin = __esm({
798
+ "src/lib/admin.ts"() {
799
+ "use strict";
800
+ import_chalk16 = __toESM(require("chalk"));
801
+ init_auth();
802
+ init_trace();
803
+ init_types();
804
+ DASHBOARD_BASE3 = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
805
+ }
806
+ });
807
+
808
+ // src/lib/tenant-pick.ts
809
+ var tenant_pick_exports = {};
810
+ __export(tenant_pick_exports, {
811
+ pickTenant: () => pickTenant
812
+ });
813
+ async function fetchPage(teamId, q) {
814
+ const out = await admin({
815
+ method: "GET",
816
+ path: `/teams/${encodeURIComponent(teamId)}/tenants?detail=1&limit=${PAGE}${q ? `&q=${encodeURIComponent(q)}` : ""}`,
817
+ summary: q ? `Search tenants matching "${q}"` : "List tenants (first page)"
818
+ });
819
+ const rows = (out?.tenants ?? []).map(
820
+ (t) => typeof t === "string" ? { tenant_name: t } : t
821
+ );
822
+ return { rows, total: out?.total ?? rows.length, defaultTenant: out?.default_tenant ?? null };
823
+ }
824
+ function label(t, defaultTenant, active) {
825
+ const tags = [
826
+ t.tenant_name === active ? import_chalk22.default.cyan("active scope") : "",
827
+ t.tenant_name === defaultTenant ? import_chalk22.default.dim("team default") : ""
828
+ ].filter(Boolean).join(", ");
829
+ const disp = t.display_name && t.display_name !== t.tenant_name ? import_chalk22.default.dim(` ${t.display_name}`) : "";
830
+ return `${t.tenant_name}${disp}${tags ? ` (${tags})` : ""}`;
831
+ }
832
+ async function pickTenant(teamId, opts = {}) {
833
+ const { default: inquirer2 } = await import("inquirer");
834
+ const active = loadCredentials()?.activeTenant;
835
+ let q = opts.initialQuery ?? "";
836
+ for (; ; ) {
837
+ const spinner = (0, import_ora8.default)(q ? `Searching tenants for "${q}"...` : "Loading tenants...").start();
838
+ const page = await fetchPage(teamId, q).finally(() => spinner.stop());
839
+ if (!page.total && !q) {
840
+ if (opts.allowCreate) {
841
+ const { make } = await inquirer2.prompt([{ type: "confirm", name: "make", message: "No tenants yet \u2014 create one?", default: true }]);
842
+ if (make) return await createTenantInline(teamId);
843
+ }
844
+ console.error(import_chalk22.default.red("This team has no tenants. Create one with `apiblaze tenant create`."));
845
+ return null;
846
+ }
847
+ const truncated = page.total > page.rows.length;
848
+ const choices = page.rows.map((t) => ({
849
+ name: label(t, page.defaultTenant, active),
850
+ value: t.tenant_name
851
+ }));
852
+ if (truncated || q) {
853
+ choices.push(new inquirer2.Separator(import_chalk22.default.dim(
854
+ truncated ? `showing ${page.rows.length} of ${page.total}${q ? ` matching "${q}"` : ""} \u2014 search to narrow` : `matches for "${q}"`
855
+ )));
856
+ choices.push({ name: `\u{1F50D} Search${q ? " again" : ""}\u2026`, value: "\0search" });
857
+ }
858
+ if (q) choices.push({ name: "\u21BA Show all (clear search)", value: "\0clear" });
859
+ if (opts.allowCreate) choices.push({ name: "\uFF0B Create a new tenant\u2026", value: "\0create" });
860
+ if (opts.allowBack) choices.push({ name: "\u2190 Back", value: "\0back" });
861
+ const { picked } = await inquirer2.prompt([{
862
+ type: "list",
863
+ name: "picked",
864
+ pageSize: PAGE + 5,
865
+ message: opts.message ?? "Which tenant?",
866
+ default: active && page.rows.some((t) => t.tenant_name === active) ? active : void 0,
867
+ choices
868
+ }]);
869
+ if (picked === "\0back") return null;
870
+ if (picked === "\0clear") {
871
+ q = "";
872
+ continue;
873
+ }
874
+ if (picked === "\0create") return await createTenantInline(teamId);
875
+ if (picked === "\0search") {
876
+ const { nq } = await inquirer2.prompt([{ type: "input", name: "nq", message: "Search (name or display name):", default: q }]);
877
+ q = String(nq ?? "").trim();
878
+ continue;
879
+ }
880
+ return picked;
881
+ }
882
+ }
883
+ async function createTenantInline(teamId) {
884
+ const { default: inquirer2 } = await import("inquirer");
885
+ for (; ; ) {
886
+ const { name } = await inquirer2.prompt([{
887
+ type: "input",
888
+ name: "name",
889
+ message: `Tenant name ${import_chalk22.default.dim("(lowercase letters/numbers; globally unique \u2014 becomes {name}.portal.apiblaze.com)")}:`,
890
+ validate: (s) => /^[a-z0-9]+$/.test(s.trim()) ? true : "lowercase letters and numbers only"
891
+ }]);
892
+ const slugInput = name.trim();
893
+ try {
894
+ const out = await admin({
895
+ method: "POST",
896
+ path: `/teams/${encodeURIComponent(teamId)}/tenants`,
897
+ body: { tenant_name: slugInput, display_name: slugInput },
898
+ summary: `Create tenant "${slugInput}"`
899
+ });
900
+ const slug = out?.tenant_name ?? out?.tenant?.tenant_name ?? slugInput;
901
+ console.log(import_chalk22.default.green(` Tenant ${import_chalk22.default.bold(slug)} created.`));
902
+ return slug;
903
+ } catch (err) {
904
+ const msg = err instanceof Error ? err.message : String(err);
905
+ if (/taken|unique|exists|reserved|conflict/i.test(msg)) {
906
+ console.log(import_chalk22.default.yellow(` "${slugInput}" is not available (tenant names are global): ${msg}`));
907
+ continue;
908
+ }
909
+ throw err;
910
+ }
911
+ }
477
912
  }
478
913
  var import_chalk22, import_ora8, PAGE;
479
914
  var init_tenant_pick = __esm({
@@ -492,7 +927,7 @@ var import_commander = require("commander");
492
927
  var import_chalk34 = __toESM(require("chalk"));
493
928
 
494
929
  // package.json
495
- var version = "0.12.1";
930
+ var version = "0.15.0";
496
931
 
497
932
  // src/index.ts
498
933
  init_types();
@@ -501,43 +936,7 @@ init_types();
501
936
  var import_chalk2 = __toESM(require("chalk"));
502
937
  var import_ora = __toESM(require("ora"));
503
938
  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
939
+ init_team();
541
940
  var DASHBOARD_BASE2 = "https://dashboard.apiblaze.com";
542
941
  function openBrowser(url) {
543
942
  const { exec } = require("child_process");
@@ -613,7 +1012,12 @@ async function runLogin() {
613
1012
  }
614
1013
  let teamId = defaultTeamId ?? void 0;
615
1014
  let teamName;
616
- const linked = await resolveLinkedTeam({ preferredId: defaultTeamId ?? void 0, interactive: !!process.stdin.isTTY });
1015
+ const linked = await resolveLinkedTeam({
1016
+ preferredId: defaultTeamId ?? void 0,
1017
+ interactive: !!process.stdin.isTTY,
1018
+ // >1 team → ASK (JWT default is just the cursor). One team → link silently.
1019
+ promptWhenMultiple: true
1020
+ });
617
1021
  if (linked) {
618
1022
  teamId = linked.teamId;
619
1023
  teamName = linked.teamName;
@@ -838,6 +1242,9 @@ function formatCapturedRequest(req, note) {
838
1242
  ].join("\n");
839
1243
  }
840
1244
 
1245
+ // src/commands/dev.ts
1246
+ init_team();
1247
+
841
1248
  // src/lib/random-name.ts
842
1249
  var ADJECTIVES = [
843
1250
  "amber",
@@ -1178,587 +1585,253 @@ async function offerAutoCreate(teamId, port) {
1178
1585
  const targets = await getLocalhostTargets(teamId).catch(() => []);
1179
1586
  const created = targets.find((t) => t.projectId === result.project_id);
1180
1587
  if (!created) {
1181
- console.log(import_chalk4.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
1182
- return null;
1183
- }
1184
- return created;
1185
- }
1186
- function isInternalTarget(url) {
1187
- if (!url) return false;
1188
- try {
1189
- const h = new URL(url).hostname.toLowerCase();
1190
- return h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "0.0.0.0" || /^127\./.test(h) || /^10\./.test(h) || /^192\.168\./.test(h) || /^169\.254\./.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h);
1191
- } catch {
1192
- return false;
1193
- }
1194
- }
1195
- function printTunnelEndpoints(restore, targets) {
1196
- if (restore.length === 0) return;
1197
- console.log(import_chalk4.default.bold("\nYour proxy is live at:"));
1198
- for (const r of restore) {
1199
- const label2 = targets.find((t) => t.projectId === r.projectId)?.projectName ?? r.projectId;
1200
- console.log(`
1201
- ${import_chalk4.default.bold(label2)}`);
1202
- const internalEnvs = Object.keys(r.environments ?? {}).filter((e) => isInternalTarget(r.environments[e]?.target));
1203
- const envs = internalEnvs.includes("dev") ? ["dev"] : internalEnvs.length ? internalEnvs : ["dev"];
1204
- for (const env of envs) {
1205
- console.log(` ${import_chalk4.default.dim("API: ")} ${import_chalk4.default.cyan(`https://${r.projectId}.abz.run/${r.apiVersion}/${env}/`)}`);
1206
- }
1207
- if (r.tenant) {
1208
- console.log(` ${import_chalk4.default.dim("Portal:")} ${import_chalk4.default.cyan(`https://${r.tenant}.portal.apiblaze.com/${r.apiVersion}`)}`);
1209
- }
1210
- }
1211
- }
1212
- async function probeLocalServer(port) {
1213
- const controller = new AbortController();
1214
- const timer = setTimeout(() => controller.abort(), 1500);
1215
- try {
1216
- await fetch(`http://127.0.0.1:${port}/`, { method: "HEAD", signal: controller.signal });
1217
- return true;
1218
- } catch (err) {
1219
- if (err?.name === "AbortError") return true;
1220
- const code = err?.cause?.code;
1221
- return !(code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH");
1222
- } finally {
1223
- clearTimeout(timer);
1224
- }
1225
- }
1226
- async function runDev(options) {
1227
- const creds = await ensureLoggedIn(!!process.stdin.isTTY);
1228
- const linked = await resolveLinkedTeam({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
1229
- if (!linked) {
1230
- console.error(import_chalk4.default.red("No team available. Run `apiblaze login` to set up your team."));
1231
- process.exit(1);
1232
- }
1233
- const teamId = linked.teamId;
1234
- if (linked.teamId !== creds.teamId || linked.teamName !== creds.teamName) {
1235
- saveCredentials({ ...creds, teamId: linked.teamId, teamName: linked.teamName });
1236
- }
1237
- if (linked.teamName) {
1238
- console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(linked.teamName)}`);
1239
- }
1240
- let targets;
1241
- {
1242
- const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
1243
- try {
1244
- targets = await getLocalhostTargets(teamId);
1245
- spinner.stop();
1246
- } catch (err) {
1247
- spinner.fail("Failed to fetch projects.");
1248
- throw err;
1249
- }
1250
- }
1251
- let selectedTargets;
1252
- if (targets.length === 0) {
1253
- const created = await offerAutoCreate(teamId, options.port);
1254
- if (!created) {
1255
- console.log("Set a project's upstream target to localhost or a private IP, then try again.");
1256
- process.exit(0);
1257
- }
1258
- selectedTargets = [created];
1259
- } else if (targets.length === 1) {
1260
- const { confirmed } = await import_inquirer.default.prompt([{
1261
- type: "confirm",
1262
- name: "confirmed",
1263
- message: `Found 1 project with an internal target \u2014 tunnel "${import_chalk4.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
1264
- default: true
1265
- }]);
1266
- if (!confirmed) {
1267
- console.log("Aborted.");
1268
- process.exit(0);
1269
- }
1270
- selectedTargets = targets;
1271
- } else {
1272
- const ALL = "__all__";
1273
- const { chosen } = await import_inquirer.default.prompt([{
1274
- type: "list",
1275
- name: "chosen",
1276
- message: `Found ${targets.length} projects with an internal target \u2014 pick one to tunnel:`,
1277
- choices: [
1278
- ...targets.map((t) => ({
1279
- name: `${import_chalk4.default.bold(t.projectName)} (${t.tenantName}) \u2014 ${t.target}`,
1280
- value: t
1281
- })),
1282
- new import_inquirer.default.Separator(),
1283
- { name: `Tunnel all ${targets.length}`, value: ALL }
1284
- ]
1285
- }]);
1286
- selectedTargets = chosen === ALL ? targets : [chosen];
1287
- }
1288
- console.log(
1289
- import_chalk4.default.green(`
1290
- Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
1291
- `)
1292
- );
1293
- let recordSink;
1294
- let captureStream;
1295
- if (options.captureFile) {
1296
- captureStream = import_fs.default.createWriteStream(options.captureFile, { flags: "a" });
1297
- recordSink = (r) => captureStream.write(JSON.stringify(r) + "\n");
1298
- console.log(import_chalk4.default.gray(`Streaming full traffic to ${options.captureFile}
1299
- `));
1300
- }
1301
- let restore = [];
1302
- let connect;
1303
- {
1304
- const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
1305
- try {
1306
- const result = await putDevTunnel({
1307
- targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
1308
- });
1309
- restore = result.restore ?? [];
1310
- connect = result.connect;
1311
- spinner.succeed("Tunnel registered.");
1312
- } catch (err) {
1313
- spinner.fail("Failed to register tunnel.");
1314
- throw err;
1315
- }
1316
- }
1317
- printTunnelEndpoints(restore, selectedTargets);
1318
- const clients = connect.projects.map(
1319
- (projectId) => startTunnelClient({
1320
- connectUrl: connect.url,
1321
- token: connect.token,
1322
- projectId,
1323
- localPort: options.port,
1324
- onEntry: (entry) => console.log(formatLogLine(entry)),
1325
- onStatus: (status) => console.log(import_chalk4.default.gray(`[${projectId}] ${status}`)),
1326
- onCapture: (req, note) => console.log(formatCapturedRequest(req, note)),
1327
- onCaptureStart: () => console.log(
1328
- import_chalk4.default.magenta(`
1329
- \u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
1330
- `)
1331
- ),
1332
- onResume: () => console.log(
1333
- import_chalk4.default.green(`
1334
- \u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
1335
- `)
1336
- ),
1337
- onRecord: recordSink
1338
- })
1339
- );
1340
- const localUp = await probeLocalServer(options.port);
1341
- console.log("\n" + import_chalk4.default.gray("\u2500".repeat(60)));
1342
- console.log(import_chalk4.default.bold("Live traffic") + import_chalk4.default.gray(" (Ctrl+C to stop)"));
1343
- console.log(
1344
- localUp ? import_chalk4.default.green(`\u2713 Local server detected on port ${options.port} \u2014 forwarding live.`) : import_chalk4.default.magenta(`\u26B2 Nothing listening on port ${options.port} yet \u2014 requests will be captured until your server starts.`)
1345
- );
1346
- console.log(import_chalk4.default.gray("\u2500".repeat(60)) + "\n");
1347
- let isCleaningUp = false;
1348
- async function cleanup() {
1349
- if (isCleaningUp) return;
1350
- isCleaningUp = true;
1351
- console.log(import_chalk4.default.gray("\n\nShutting down..."));
1352
- for (const client of clients) client.close();
1353
- captureStream?.end();
1354
- await deleteDevTunnel(restore).catch(() => {
1355
- });
1356
- console.log(import_chalk4.default.green("Tunnel stopped."));
1357
- process.exit(0);
1358
- }
1359
- process.on("SIGINT", () => void cleanup());
1360
- process.on("SIGTERM", () => void cleanup());
1361
- await new Promise(() => {
1362
- });
1363
- }
1364
-
1365
- // src/commands/projects.ts
1366
- var import_chalk5 = __toESM(require("chalk"));
1367
- var import_ora3 = __toESM(require("ora"));
1368
- init_auth();
1369
- init_api();
1370
- async function runProjects() {
1371
- const creds = loadCredentials();
1372
- if (!creds) {
1373
- console.error(import_chalk5.default.red("Not logged in. Run `apiblaze login` first."));
1374
- process.exit(1);
1375
- }
1376
- if (creds.githubHandle) {
1377
- console.log(`${import_chalk5.default.cyan("\u2192")} Logged in as ${import_chalk5.default.bold("@" + creds.githubHandle)}`);
1378
- }
1379
- let teamId = creds.teamId;
1380
- let teamName = creds.teamName;
1381
- if (!teamId) {
1382
- const teams = await getTeams().catch(() => []);
1383
- if (teams.length === 1) {
1384
- teamId = teams[0].teamId;
1385
- teamName = teams[0].name;
1386
- } else if (teams.length > 1) {
1387
- const { default: inquirer2 } = await import("inquirer");
1388
- const { chosen } = await inquirer2.prompt([{
1389
- type: "list",
1390
- name: "chosen",
1391
- message: "Which team do you want to use?",
1392
- choices: teams.map((t) => ({ name: t.name, value: t.teamId }))
1393
- }]);
1394
- teamId = chosen;
1395
- teamName = teams.find((t) => t.teamId === chosen)?.name;
1396
- }
1397
- }
1398
- if (!teamId) {
1399
- console.error(import_chalk5.default.red("No team found. Run `apiblaze login` to set up your team."));
1400
- process.exit(1);
1401
- }
1402
- console.log(`${import_chalk5.default.cyan("\u2192")} Team: ${import_chalk5.default.bold(teamName ?? teamId)}
1403
- `);
1404
- const spinner = (0, import_ora3.default)("Fetching projects...").start();
1405
- let projects;
1406
- try {
1407
- projects = await getProjects(teamId);
1408
- spinner.stop();
1409
- } catch (err) {
1410
- spinner.fail("Failed to fetch projects.");
1411
- throw err;
1412
- }
1413
- if (projects.length === 0) {
1414
- console.log(import_chalk5.default.yellow("No projects found for this team."));
1415
- return;
1416
- }
1417
- const width = Math.max(...projects.map((p) => p.projectName.length));
1418
- for (const p of projects) {
1419
- console.log(` ${import_chalk5.default.bold(p.projectName.padEnd(width))} ${import_chalk5.default.dim("v" + p.apiVersion)}`);
1420
- }
1421
- console.log(import_chalk5.default.dim(`
1422
- ${projects.length} project${projects.length === 1 ? "" : "s"}`));
1423
- }
1424
-
1425
- // src/commands/create.ts
1426
- var import_fs2 = __toESM(require("fs"));
1427
- var import_chalk6 = __toESM(require("chalk"));
1428
- var import_ora4 = __toESM(require("ora"));
1429
- init_auth();
1430
- init_api();
1431
- function normalizeName(raw) {
1432
- return (raw || "").toLowerCase().replace(/[^a-z0-9]/g, "");
1433
- }
1434
- function isHttpUrl(s) {
1435
- try {
1436
- const u = new URL((s || "").trim());
1437
- return u.protocol === "http:" || u.protocol === "https:";
1438
- } catch {
1439
- return false;
1588
+ console.log(import_chalk4.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
1589
+ return null;
1440
1590
  }
1591
+ return created;
1441
1592
  }
1442
- function stripTenantFromPortal(devPortal) {
1593
+ function isInternalTarget(url) {
1594
+ if (!url) return false;
1443
1595
  try {
1444
- const u = new URL(devPortal);
1445
- const dot = u.hostname.indexOf(".");
1446
- if (dot < 0) return devPortal;
1447
- const product = u.hostname.slice(0, dot).split("-")[0];
1448
- u.hostname = `${product}${u.hostname.slice(dot)}`;
1449
- return u.toString();
1596
+ const h = new URL(url).hostname.toLowerCase();
1597
+ return h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "0.0.0.0" || /^127\./.test(h) || /^10\./.test(h) || /^192\.168\./.test(h) || /^169\.254\./.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h);
1450
1598
  } catch {
1451
- return devPortal;
1599
+ return false;
1452
1600
  }
1453
1601
  }
1454
- function fail(message) {
1455
- console.error(import_chalk6.default.red(`Error: ${message}`));
1456
- process.exit(1);
1457
- }
1458
- function buildTryItCurl(url, authType, apiKey) {
1459
- if (authType === "api_key") {
1460
- if (!apiKey) return null;
1461
- return `curl ${url} -H "X-API-Key: ${apiKey}"`;
1602
+ function printTunnelEndpoints(restore, targets) {
1603
+ if (restore.length === 0) return;
1604
+ console.log(import_chalk4.default.bold("\nYour proxy is live at:"));
1605
+ for (const r of restore) {
1606
+ const label2 = targets.find((t) => t.projectId === r.projectId)?.projectName ?? r.projectId;
1607
+ console.log(`
1608
+ ${import_chalk4.default.bold(label2)}`);
1609
+ const internalEnvs = Object.keys(r.environments ?? {}).filter((e) => isInternalTarget(r.environments[e]?.target));
1610
+ const envs = internalEnvs.includes("dev") ? ["dev"] : internalEnvs.length ? internalEnvs : ["dev"];
1611
+ for (const env of envs) {
1612
+ console.log(` ${import_chalk4.default.dim("API: ")} ${import_chalk4.default.cyan(`https://${r.projectId}.abz.run/${r.apiVersion}/${env}/`)}`);
1613
+ }
1614
+ if (r.tenant) {
1615
+ console.log(` ${import_chalk4.default.dim("Portal:")} ${import_chalk4.default.cyan(`https://${r.tenant}.portal.apiblaze.com/${r.apiVersion}`)}`);
1616
+ }
1462
1617
  }
1463
- if (authType === "none") return `curl ${url}`;
1464
- return null;
1465
1618
  }
1466
- function printCurlExample(url, authType, apiKey, devPortal) {
1467
- const curl = buildTryItCurl(url, authType, apiKey);
1468
- console.log();
1469
- if (curl) {
1470
- console.log(` ${import_chalk6.default.dim("Try it \u2014 copy/paste:")}`);
1471
- console.log(` ${import_chalk6.default.cyan(curl)}`);
1472
- } else if (authType === "oauth") {
1473
- console.log(` ${import_chalk6.default.dim("Try it:")} this proxy uses OAuth \u2014 sign in at ${import_chalk6.default.bold(devPortal ?? "the dev portal")} to get a token,`);
1474
- console.log(` ${import_chalk6.default.dim(`then call ${url} with`)} ${import_chalk6.default.cyan('-H "Authorization: Bearer <token>"')}`);
1619
+ async function probeLocalServer(port) {
1620
+ const controller = new AbortController();
1621
+ const timer = setTimeout(() => controller.abort(), 1500);
1622
+ try {
1623
+ await fetch(`http://127.0.0.1:${port}/`, { method: "HEAD", signal: controller.signal });
1624
+ return true;
1625
+ } catch (err) {
1626
+ if (err?.name === "AbortError") return true;
1627
+ const code = err?.cause?.code;
1628
+ return !(code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH");
1629
+ } finally {
1630
+ clearTimeout(timer);
1475
1631
  }
1476
1632
  }
1477
- var VALID_AUTH = ["api_key", "none", "oauth"];
1478
- async function runCreate(opts = {}) {
1479
- const creds = loadCredentials();
1480
- if (!creds) {
1481
- await runAnonymousCreate(opts);
1482
- return;
1633
+ async function runDev(options) {
1634
+ const creds = await ensureLoggedIn(!!process.stdin.isTTY);
1635
+ const linked = await resolveLinkedTeam({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
1636
+ if (!linked) {
1637
+ console.error(import_chalk4.default.red("No team available. Run `apiblaze login` to set up your team."));
1638
+ process.exit(1);
1483
1639
  }
1484
- const interactive = !!process.stdin.isTTY && !opts.json;
1485
- const auth = (opts.auth ?? "api_key").toLowerCase();
1486
- if (!VALID_AUTH.includes(auth)) {
1487
- fail(`Invalid --auth "${auth}". Use one of: ${VALID_AUTH.join(", ")}.`);
1640
+ const teamId = linked.teamId;
1641
+ if (linked.teamId !== creds.teamId || linked.teamName !== creds.teamName) {
1642
+ saveCredentials({ ...creds, teamId: linked.teamId, teamName: linked.teamName });
1488
1643
  }
1489
- let teamId = creds.teamId;
1490
- if (opts.team) {
1491
- if (opts.team.startsWith("team_")) {
1492
- teamId = opts.team;
1493
- } else {
1494
- const teams = await getTeams().catch(() => []);
1495
- const match = teams.find(
1496
- (t) => t.teamId === opts.team || t.name.toLowerCase() === opts.team.toLowerCase()
1497
- );
1498
- if (!match) {
1499
- fail(`Team "${opts.team}" not found. Run \`apiblaze team\` to see your teams.`);
1500
- }
1501
- teamId = match.teamId;
1502
- }
1644
+ if (linked.teamName) {
1645
+ console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(linked.teamName)}`);
1503
1646
  }
1504
- if (!opts.json) console.log(import_chalk6.default.bold("\nCreate an API proxy\n"));
1505
- let name = "";
1506
- if (opts.name !== void 0) {
1507
- name = normalizeName(opts.name);
1508
- if (name.length < 3) fail("Proxy name must be at least 3 characters (letters and digits only).");
1509
- const check = await checkProxyName(name, teamId, opts.apiversion).catch(() => null);
1510
- if (check && (!check.canUseProjectName || !check.canUseApiVersion)) {
1511
- fail(`Proxy name "${name}" is not available${check.message ? ` \u2014 ${check.message}` : ""}.`);
1512
- }
1513
- } else if (interactive) {
1514
- const { default: inquirer2 } = await import("inquirer");
1515
- for (; ; ) {
1516
- const { rawName } = await inquirer2.prompt([{
1517
- type: "input",
1518
- name: "rawName",
1519
- message: "Proxy name (your API will live at <name>.abz.run):",
1520
- transformer: (v) => normalizeName(v)
1521
- }]);
1522
- name = normalizeName(rawName);
1523
- if (name.length < 3) {
1524
- console.log(import_chalk6.default.yellow(" Name must be at least 3 characters (letters and digits only).\n"));
1525
- continue;
1526
- }
1527
- const spinner2 = (0, import_ora4.default)("Checking availability...").start();
1528
- try {
1529
- const check = await checkProxyName(name, teamId, opts.apiversion);
1530
- spinner2.stop();
1531
- if (!check.canUseProjectName || !check.canUseApiVersion) {
1532
- console.log(import_chalk6.default.yellow(` "${name}" is not available${check.message ? ` \u2014 ${check.message}` : ""}. Try another.
1533
- `));
1534
- continue;
1535
- }
1536
- } catch {
1537
- spinner2.stop();
1538
- console.log(import_chalk6.default.dim(" (could not verify availability; continuing)"));
1539
- }
1540
- console.log(`${import_chalk6.default.cyan("\u2192")} Your API will live at ${import_chalk6.default.bold(`https://${name}.abz.run`)}
1541
- `);
1542
- break;
1647
+ let targets;
1648
+ {
1649
+ const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
1650
+ try {
1651
+ targets = await getLocalhostTargets(teamId);
1652
+ spinner.stop();
1653
+ } catch (err) {
1654
+ spinner.fail("Failed to fetch projects.");
1655
+ throw err;
1543
1656
  }
1544
- } else {
1545
- fail("--name is required in non-interactive mode.");
1546
1657
  }
1547
- let targetUrl = "";
1548
- if (opts.target !== void 0) {
1549
- if (!isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
1550
- targetUrl = opts.target.trim();
1551
- } else if (interactive) {
1552
- const { default: inquirer2 } = await import("inquirer");
1553
- for (; ; ) {
1554
- const { url } = await inquirer2.prompt([{
1555
- type: "input",
1556
- name: "url",
1557
- message: "Target URL to forward requests to (e.g. https://httpbin.org):"
1558
- }]);
1559
- if (!isHttpUrl(url)) {
1560
- console.log(import_chalk6.default.yellow(" Enter a valid http(s) URL.\n"));
1561
- continue;
1562
- }
1563
- targetUrl = url.trim();
1564
- break;
1658
+ let selectedTargets;
1659
+ if (targets.length === 0) {
1660
+ const created = await offerAutoCreate(teamId, options.port);
1661
+ if (!created) {
1662
+ console.log("Set a project's upstream target to localhost or a private IP, then try again.");
1663
+ process.exit(0);
1565
1664
  }
1566
- } else {
1567
- fail("--target is required in non-interactive mode.");
1568
- }
1569
- if (interactive && !opts.yes) {
1570
- const { default: inquirer2 } = await import("inquirer");
1571
- console.log(`${import_chalk6.default.cyan("\u2192")} Auth: ${import_chalk6.default.bold(auth)}${auth === "api_key" ? " \u2014 consumers send an X-API-Key header" : ""}`);
1572
- const { ok } = await inquirer2.prompt([{
1665
+ selectedTargets = [created];
1666
+ } else if (targets.length === 1) {
1667
+ const { confirmed } = await import_inquirer.default.prompt([{
1573
1668
  type: "confirm",
1574
- name: "ok",
1575
- message: `Create proxy "${name}" \u2192 ${targetUrl}?`,
1669
+ name: "confirmed",
1670
+ message: `Found 1 project with an internal target \u2014 tunnel "${import_chalk4.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
1576
1671
  default: true
1577
1672
  }]);
1578
- if (!ok) {
1579
- console.log(import_chalk6.default.yellow("Cancelled."));
1580
- return;
1581
- }
1582
- }
1583
- const spinner = !opts.json ? (0, import_ora4.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
1584
- let result;
1585
- try {
1586
- result = await createProxy({ name, target_url: targetUrl, auth_type: auth, team_id: teamId, ...opts.apiversion ? { api_version: opts.apiversion } : {} });
1587
- spinner?.succeed(import_chalk6.default.green("Proxy created!"));
1588
- } catch (err) {
1589
- spinner?.fail("Failed to create proxy.");
1590
- throw err;
1591
- }
1592
- const version2 = result.api_version || "1.0.0";
1593
- const keys = result.api_keys ?? {};
1594
- const adminKey = keys.dev ?? Object.values(keys)[0];
1595
- const proxyUrl = `https://${name}.abz.run/${version2}/dev`;
1596
- const devPortal = result.devPortal ? stripTenantFromPortal(result.devPortal) : void 0;
1597
- if (opts.json) {
1598
- process.stdout.write(JSON.stringify({
1599
- project_id: result.project_id,
1600
- api_version: version2,
1601
- proxy_url: proxyUrl,
1602
- dev_portal: devPortal,
1603
- api_key: adminKey,
1604
- api_keys: keys,
1605
- team_id: teamId
1606
- }) + "\n");
1607
- return;
1608
- }
1609
- console.log();
1610
- console.log(` ${import_chalk6.default.dim("Proxy URL: ")} ${import_chalk6.default.bold(proxyUrl)}`);
1611
- if (devPortal) console.log(` ${import_chalk6.default.dim("Dev portal:")} ${import_chalk6.default.bold(devPortal)}`);
1612
- if (adminKey) {
1613
- console.log();
1614
- console.log(` ${import_chalk6.default.dim("Consumer admin API key (dev):")}`);
1615
- console.log(` ${import_chalk6.default.bold.green(adminKey)}`);
1616
- console.log(import_chalk6.default.dim("\n Save this now \u2014 send it as the X-API-Key header. It may not be shown again."));
1617
- const otherEnvs = Object.keys(keys).filter((e) => e !== "dev");
1618
- if (otherEnvs.length) {
1619
- console.log(import_chalk6.default.dim(` (Separate keys were also created for: ${otherEnvs.join(", ")}.)`));
1673
+ if (!confirmed) {
1674
+ console.log("Aborted.");
1675
+ process.exit(0);
1620
1676
  }
1677
+ selectedTargets = targets;
1678
+ } else {
1679
+ const ALL = "__all__";
1680
+ const { chosen } = await import_inquirer.default.prompt([{
1681
+ type: "list",
1682
+ name: "chosen",
1683
+ message: `Found ${targets.length} projects with an internal target \u2014 pick one to tunnel:`,
1684
+ choices: [
1685
+ ...targets.map((t) => ({
1686
+ name: `${import_chalk4.default.bold(t.projectName)} (${t.tenantName}) \u2014 ${t.target}`,
1687
+ value: t
1688
+ })),
1689
+ new import_inquirer.default.Separator(),
1690
+ { name: `Tunnel all ${targets.length}`, value: ALL }
1691
+ ]
1692
+ }]);
1693
+ selectedTargets = chosen === ALL ? targets : [chosen];
1621
1694
  }
1622
- printCurlExample(proxyUrl, auth, adminKey, devPortal);
1623
- console.log();
1624
- }
1625
- async function runAnonymousCreate(opts) {
1626
- const interactive = !!process.stdin.isTTY && !opts.json;
1627
- if (!opts.json) {
1628
- console.log(import_chalk6.default.bold("\nCreate an API proxy"));
1629
- console.log(import_chalk6.default.dim("Not logged in \u2014 creating an anonymous proxy. You can claim it to your account within 30 days.\n"));
1695
+ console.log(
1696
+ import_chalk4.default.green(`
1697
+ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
1698
+ `)
1699
+ );
1700
+ let recordSink;
1701
+ let captureStream;
1702
+ if (options.captureFile) {
1703
+ captureStream = import_fs.default.createWriteStream(options.captureFile, { flags: "a" });
1704
+ recordSink = (r) => captureStream.write(JSON.stringify(r) + "\n");
1705
+ console.log(import_chalk4.default.gray(`Streaming full traffic to ${options.captureFile}
1706
+ `));
1630
1707
  }
1631
- let body = {};
1632
- if (opts.config) {
1633
- let raw = "";
1634
- try {
1635
- raw = import_fs2.default.readFileSync(opts.config, "utf8");
1636
- } catch {
1637
- fail(`Cannot read --config file: ${opts.config}`);
1638
- }
1639
- let parsed;
1708
+ let restore = [];
1709
+ let connect;
1710
+ {
1711
+ const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
1640
1712
  try {
1641
- parsed = JSON.parse(raw);
1642
- } catch {
1643
- fail(`--config is not valid JSON: ${opts.config}`);
1713
+ const result = await putDevTunnel({
1714
+ targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
1715
+ });
1716
+ restore = result.restore ?? [];
1717
+ connect = result.connect;
1718
+ spinner.succeed("Tunnel registered.");
1719
+ } catch (err) {
1720
+ spinner.fail("Failed to register tunnel.");
1721
+ throw err;
1644
1722
  }
1645
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) fail("--config must be a JSON object.");
1646
- body = parsed;
1647
1723
  }
1648
- let name = opts.name !== void 0 ? normalizeName(opts.name) : typeof body.name === "string" ? body.name : void 0;
1649
- if (opts.name !== void 0 && name.length < 3) {
1650
- fail("Proxy name must be at least 3 characters (letters and digits only).");
1724
+ printTunnelEndpoints(restore, selectedTargets);
1725
+ const clients = connect.projects.map(
1726
+ (projectId) => startTunnelClient({
1727
+ connectUrl: connect.url,
1728
+ token: connect.token,
1729
+ projectId,
1730
+ localPort: options.port,
1731
+ onEntry: (entry) => console.log(formatLogLine(entry)),
1732
+ onStatus: (status) => console.log(import_chalk4.default.gray(`[${projectId}] ${status}`)),
1733
+ onCapture: (req, note) => console.log(formatCapturedRequest(req, note)),
1734
+ onCaptureStart: () => console.log(
1735
+ import_chalk4.default.magenta(`
1736
+ \u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
1737
+ `)
1738
+ ),
1739
+ onResume: () => console.log(
1740
+ import_chalk4.default.green(`
1741
+ \u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
1742
+ `)
1743
+ ),
1744
+ onRecord: recordSink
1745
+ })
1746
+ );
1747
+ const localUp = await probeLocalServer(options.port);
1748
+ console.log("\n" + import_chalk4.default.gray("\u2500".repeat(60)));
1749
+ console.log(import_chalk4.default.bold("Live traffic") + import_chalk4.default.gray(" (Ctrl+C to stop)"));
1750
+ console.log(
1751
+ localUp ? import_chalk4.default.green(`\u2713 Local server detected on port ${options.port} \u2014 forwarding live.`) : import_chalk4.default.magenta(`\u26B2 Nothing listening on port ${options.port} yet \u2014 requests will be captured until your server starts.`)
1752
+ );
1753
+ console.log(import_chalk4.default.gray("\u2500".repeat(60)) + "\n");
1754
+ let isCleaningUp = false;
1755
+ async function cleanup() {
1756
+ if (isCleaningUp) return;
1757
+ isCleaningUp = true;
1758
+ console.log(import_chalk4.default.gray("\n\nShutting down..."));
1759
+ for (const client of clients) client.close();
1760
+ captureStream?.end();
1761
+ await deleteDevTunnel(restore).catch(() => {
1762
+ });
1763
+ console.log(import_chalk4.default.green("Tunnel stopped."));
1764
+ process.exit(0);
1651
1765
  }
1652
- if (opts.target && !isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
1653
- let target = opts.target?.trim() || (typeof body.target === "string" ? body.target : "") || (typeof body.target_url === "string" ? body.target_url : "");
1654
- const hasOtherSource = !!(body.openapi || body.github);
1655
- if (!target && !hasOtherSource) {
1656
- if (interactive) {
1766
+ process.on("SIGINT", () => void cleanup());
1767
+ process.on("SIGTERM", () => void cleanup());
1768
+ await new Promise(() => {
1769
+ });
1770
+ }
1771
+
1772
+ // src/commands/projects.ts
1773
+ var import_chalk5 = __toESM(require("chalk"));
1774
+ var import_ora3 = __toESM(require("ora"));
1775
+ init_auth();
1776
+ init_api();
1777
+ async function runProjects() {
1778
+ const creds = loadCredentials();
1779
+ if (!creds) {
1780
+ console.error(import_chalk5.default.red("Not logged in. Run `apiblaze login` first."));
1781
+ process.exit(1);
1782
+ }
1783
+ if (creds.githubHandle) {
1784
+ console.log(`${import_chalk5.default.cyan("\u2192")} Logged in as ${import_chalk5.default.bold("@" + creds.githubHandle)}`);
1785
+ }
1786
+ let teamId = creds.teamId;
1787
+ let teamName = creds.teamName;
1788
+ if (!teamId) {
1789
+ const teams = await getTeams().catch(() => []);
1790
+ if (teams.length === 1) {
1791
+ teamId = teams[0].teamId;
1792
+ teamName = teams[0].name;
1793
+ } else if (teams.length > 1) {
1657
1794
  const { default: inquirer2 } = await import("inquirer");
1658
- if (name === void 0) {
1659
- const { rawName } = await inquirer2.prompt([{
1660
- type: "input",
1661
- name: "rawName",
1662
- message: "Proxy name (leave blank to auto-generate):",
1663
- transformer: (v) => normalizeName(v)
1664
- }]);
1665
- const n = normalizeName(rawName);
1666
- name = n.length >= 3 ? n : void 0;
1667
- }
1668
- for (; ; ) {
1669
- const { url } = await inquirer2.prompt([{
1670
- type: "input",
1671
- name: "url",
1672
- message: "Target URL to forward requests to (e.g. https://httpbin.org):"
1673
- }]);
1674
- if (!isHttpUrl(url)) {
1675
- console.log(import_chalk6.default.yellow(" Enter a valid http(s) URL.\n"));
1676
- continue;
1677
- }
1678
- target = url.trim();
1679
- break;
1680
- }
1681
- } else {
1682
- fail("A source is required: pass --target, or target/openapi/github in --config.");
1795
+ const { chosen } = await inquirer2.prompt([{
1796
+ type: "list",
1797
+ name: "chosen",
1798
+ message: "Which team do you want to use?",
1799
+ choices: teams.map((t) => ({ name: t.name, value: t.teamId }))
1800
+ }]);
1801
+ teamId = chosen;
1802
+ teamName = teams.find((t) => t.teamId === chosen)?.name;
1683
1803
  }
1684
1804
  }
1685
- if (target) {
1686
- body.target = target;
1687
- body.target_url = target;
1688
- }
1689
- if (name) {
1690
- body.name = name;
1691
- body.subdomain = name;
1805
+ if (!teamId) {
1806
+ console.error(import_chalk5.default.red("No team found. Run `apiblaze login` to set up your team."));
1807
+ process.exit(1);
1692
1808
  }
1693
- if (opts.subdomain) body.subdomain = normalizeName(opts.subdomain);
1694
- if (opts.tenant) body.tenant = normalizeName(opts.tenant);
1695
- if (opts.product) body.product_slug = normalizeName(opts.product);
1696
- if (opts.displayName) body.display_name = opts.displayName;
1697
- if (opts.apiversion) body.api_version = opts.apiversion;
1698
- if (opts.auth && opts.auth !== "api_key") body.auth_type = opts.auth;
1699
- const { loadAnonCred: loadAnonCred2, saveAnonCred: saveAnonCred2, clearAnonCred: clearAnonCred2, cpFetch: cpFetch2 } = await Promise.resolve().then(() => (init_anon_cred(), anon_cred_exports));
1700
- if (opts.newSession) clearAnonCred2();
1701
- const cred = loadAnonCred2();
1702
- const spinner = !opts.json ? (0, import_ora4.default)(cred ? "Creating proxy (in your anonymous workspace)..." : "Creating proxy...").start() : null;
1703
- let result;
1809
+ console.log(`${import_chalk5.default.cyan("\u2192")} Team: ${import_chalk5.default.bold(teamName ?? teamId)}
1810
+ `);
1811
+ const spinner = (0, import_ora3.default)("Fetching projects...").start();
1812
+ let projects;
1704
1813
  try {
1705
- if (cred) {
1706
- result = await cpFetch2(cred.cp_key, "/projects", { method: "POST", body: JSON.stringify(body) });
1707
- if (Array.isArray(result.endpoints)) {
1708
- result.endpoints = result.endpoints.map((e) => e.replace(/:\/\/[^/]+/, `://${result.project_id}.tryabz.run`));
1709
- }
1710
- } else {
1711
- result = await createProxyAnonymous(body);
1712
- if (result.cp_key && result.team_id) {
1713
- saveAnonCred2(result.cp_key, result.team_id, result.claim_code);
1714
- }
1715
- }
1716
- spinner?.succeed(import_chalk6.default.green("Proxy created!"));
1814
+ projects = await getProjects(teamId);
1815
+ spinner.stop();
1717
1816
  } catch (err) {
1718
- spinner?.fail("Failed to create proxy.");
1817
+ spinner.fail("Failed to fetch projects.");
1719
1818
  throw err;
1720
1819
  }
1721
- const version2 = result.api_version || "1.0.0";
1722
- const keys = result.api_keys ?? {};
1723
- const apiKey = result.apiKey ?? keys.prod ?? Object.values(keys)[0];
1724
- const prodEndpoint = (result.endpoints || []).find((e) => e.endsWith("/prod")) || (result.endpoints || [])[0];
1725
- if (opts.json) {
1726
- process.stdout.write(JSON.stringify({
1727
- project_id: result.project_id,
1728
- api_version: version2,
1729
- endpoints: result.endpoints,
1730
- api_key: apiKey,
1731
- api_keys: keys,
1732
- claim_url: result.claim_url,
1733
- anonymous: true
1734
- }) + "\n");
1820
+ if (projects.length === 0) {
1821
+ console.log(import_chalk5.default.yellow("No projects found for this team."));
1735
1822
  return;
1736
1823
  }
1737
- console.log();
1738
- if (prodEndpoint) console.log(` ${import_chalk6.default.dim("Proxy URL: ")} ${import_chalk6.default.bold(prodEndpoint)}`);
1739
- if (result.portal) console.log(` ${import_chalk6.default.dim("Dev portal:")} ${import_chalk6.default.bold(result.portal)}`);
1740
- if (apiKey) {
1741
- console.log();
1742
- console.log(` ${import_chalk6.default.dim("API key:")}`);
1743
- console.log(` ${import_chalk6.default.bold.green(apiKey)}`);
1744
- console.log(import_chalk6.default.dim("\n Save this now \u2014 send it as the X-API-Key header. It may not be shown again."));
1745
- }
1746
- if (prodEndpoint) printCurlExample(prodEndpoint, opts.auth || "api_key", apiKey, result.portal);
1747
- const claimCode = result.claim_code || cred?.claim_code;
1748
- if (claimCode) {
1749
- console.log();
1750
- console.log(` ${import_chalk6.default.yellow("\u26A0 Anonymous \u2014 claim within 30 days or it expires.")} Everything you create`);
1751
- console.log(` with the CP key shares ONE workspace. To keep it all in one shot:`);
1752
- console.log(` ${import_chalk6.default.cyan("apiblaze login")} ${import_chalk6.default.dim("(prompts to claim your workspace into your account)")}`);
1753
- console.log(import_chalk6.default.dim(` From another machine: apiblaze claim ${claimCode} (add --team <name> to merge into an existing team)`));
1754
- } else if (result.claim_url) {
1755
- console.log();
1756
- console.log(` ${import_chalk6.default.yellow("\u26A0 Anonymous proxy \u2014 claim it to your account within 30 days or it expires:")}`);
1757
- console.log(` ${import_chalk6.default.bold(result.claim_url)}`);
1824
+ const width = Math.max(...projects.map((p) => p.projectName.length));
1825
+ for (const p of projects) {
1826
+ console.log(` ${import_chalk5.default.bold(p.projectName.padEnd(width))} ${import_chalk5.default.dim("v" + p.apiVersion)}`);
1758
1827
  }
1759
- console.log();
1828
+ console.log(import_chalk5.default.dim(`
1829
+ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
1760
1830
  }
1761
1831
 
1832
+ // src/index.ts
1833
+ init_create();
1834
+
1762
1835
  // src/commands/claim.ts
1763
1836
  var import_chalk7 = __toESM(require("chalk"));
1764
1837
  init_auth();
@@ -2858,13 +2931,28 @@ var trailingComma = /\s*,\s*/;
2858
2931
  var parseList = (s) => s.split(trailingComma).map((x) => x.trim()).filter(Boolean);
2859
2932
  async function runTenantManage(query, opts) {
2860
2933
  const { teamId } = await resolveTeam(opts.team);
2861
- const slug = opts.tenant ?? loadCredentialsTenant(query) ?? await pickTenant(teamId, { message: "Manage which tenant?", initialQuery: query, allowCreate: true });
2934
+ const slug = opts.tenant ?? await validScopedTenant(teamId, query) ?? await pickTenant(teamId, { message: "Manage which tenant?", initialQuery: query, allowCreate: true });
2862
2935
  if (!slug) return;
2863
2936
  await tenantHome(teamId, slug);
2864
2937
  }
2865
- function loadCredentialsTenant(query) {
2938
+ async function validScopedTenant(teamId, query) {
2866
2939
  if (query) return void 0;
2867
- return loadCredentials()?.activeTenant ?? void 0;
2940
+ const creds = loadCredentials();
2941
+ const scoped = creds?.activeTenant;
2942
+ if (!scoped) return void 0;
2943
+ const out = await admin({
2944
+ method: "GET",
2945
+ path: `/teams/${encodeURIComponent(teamId)}/tenants?q=${encodeURIComponent(scoped)}&limit=15`,
2946
+ summary: `Verify tenant scope ${scoped}`
2947
+ }).catch(() => null);
2948
+ const names = (out?.tenants ?? []).map((t) => typeof t === "string" ? t : t.tenant_name);
2949
+ if (names.includes(scoped)) return scoped;
2950
+ const { saveCredentials: saveCredentials2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
2951
+ const next = { ...creds };
2952
+ delete next.activeTenant;
2953
+ saveCredentials2(next);
2954
+ console.log(import_chalk24.default.yellow(`Tenant scope "${scoped}" no longer exists in this team \u2014 cleared.`));
2955
+ return void 0;
2868
2956
  }
2869
2957
  async function tenantHome(teamId, tenant2) {
2870
2958
  const { default: inquirer2 } = await import("inquirer");
@@ -3078,10 +3166,20 @@ async function clientsMenu(teamId, tenant2, base) {
3078
3166
  }]);
3079
3167
  if (pick2 === " back") return;
3080
3168
  if (pick2 === " create") {
3081
- const projects = await getProjects(teamId).catch(() => []);
3169
+ let projects = await getProjects(teamId).catch(() => []);
3082
3170
  if (!projects.length) {
3083
- console.log(import_chalk24.default.yellow(" No projects in this team \u2014 create a proxy first."));
3084
- continue;
3171
+ console.log(import_chalk24.default.yellow("\n An app client needs a proxy to derive its token identity (iss/aud), and this team has none yet."));
3172
+ const { make } = await inquirer2.prompt([{
3173
+ type: "confirm",
3174
+ name: "make",
3175
+ default: true,
3176
+ message: "Create a proxy now (interactive)?"
3177
+ }]);
3178
+ if (!make) continue;
3179
+ const { runCreate: runCreate2 } = await Promise.resolve().then(() => (init_create(), create_exports));
3180
+ await runCreate2({});
3181
+ projects = await getProjects(teamId).catch(() => []);
3182
+ if (!projects.length) continue;
3085
3183
  }
3086
3184
  const a = await inquirer2.prompt([
3087
3185
  { type: "input", name: "name", message: "Client name:", validate: (s) => !!s.trim() || "required" },
@@ -3347,6 +3445,7 @@ init_auth();
3347
3445
  var import_chalk26 = __toESM(require("chalk"));
3348
3446
  init_admin();
3349
3447
  init_api();
3448
+ init_create();
3350
3449
  async function proj(teamId, name, version2) {
3351
3450
  return resolveProject(teamId, name, version2);
3352
3451
  }
@@ -5050,16 +5149,25 @@ async function opCall(call) {
5050
5149
  function printResidue(report, applied) {
5051
5150
  const up = report?.upstash ?? {};
5052
5151
  const fga = report?.fga ?? {};
5152
+ const ghosts = report?.ghosts ?? {};
5053
5153
  console.log(import_chalk33.default.bold(applied ? "\nExternal-residue sweep" : "\nExternal residue (dry-run \u2014 nothing deleted)"));
5054
5154
  console.log(import_chalk33.default.bold("\n Upstash"));
5055
5155
  const orphans = up.orphans ?? [];
5056
5156
  if (orphans.length === 0) console.log(import_chalk33.default.green(" no orphaned keys"));
5057
5157
  for (const o of orphans) console.log(` ${import_chalk33.default.yellow(o.key)} ${import_chalk33.default.dim(`\u2014 ${o.reason}`)}`);
5058
5158
  console.log(import_chalk33.default.dim(` kept (live principals): ${up.kept ?? 0} \xB7 anon wallets (untouched): ${up.anon_wallets ?? 0}`));
5159
+ if (up.anon_wallet_detail) {
5160
+ const d = up.anon_wallet_detail;
5161
+ 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)"}`));
5162
+ }
5163
+ if (up.keyspace_census) {
5164
+ const census = Object.entries(up.keyspace_census).map(([k, v]) => `${k}=${v}`).join(" \xB7 ");
5165
+ console.log(import_chalk33.default.dim(` keyspace: ${census}`));
5166
+ }
5059
5167
  if (up.unknown?.length) console.log(import_chalk33.default.dim(` unknown (never deleted): ${up.unknown.join(", ")}`));
5060
5168
  if (applied) console.log(` ${import_chalk33.default.bold(String(up.deleted ?? 0))} key(s) deleted`);
5061
5169
  for (const e of up.errors ?? []) console.log(import_chalk33.default.red(` error: ${e}`));
5062
- console.log(import_chalk33.default.bold("\n OpenFGA / Neon"));
5170
+ console.log(import_chalk33.default.bold("\n OpenFGA / Neon \u2014 orphan stores"));
5063
5171
  if (applied) {
5064
5172
  const swept = fga?.swept ?? [];
5065
5173
  if (swept.length === 0) console.log(import_chalk33.default.green(" no orphaned stores"));
@@ -5069,6 +5177,8 @@ function printResidue(report, applied) {
5069
5177
  );
5070
5178
  }
5071
5179
  if (fga?.remaining) console.log(import_chalk33.default.yellow(` ${fga.remaining} more orphan store(s) \u2014 re-run to drain`));
5180
+ const st = fga?.side_tables;
5181
+ 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
5182
  } else {
5073
5183
  const fgaOrphans = fga?.orphans ?? [];
5074
5184
  if (fgaOrphans.length === 0) console.log(import_chalk33.default.green(" no orphaned stores"));
@@ -5077,8 +5187,26 @@ function printResidue(report, applied) {
5077
5187
  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
5188
  }
5079
5189
  console.log(import_chalk33.default.dim(` kept stores: ${(fga?.kept_store_ids ?? []).length}`));
5190
+ const st = fga?.side_tables;
5191
+ 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
5192
  }
5081
5193
  for (const e of fga?.errors ?? []) console.log(import_chalk33.default.red(` error: ${e}`));
5194
+ console.log(import_chalk33.default.bold("\n OpenFGA \u2014 ghost tuples in surviving stores"));
5195
+ if (applied) {
5196
+ if ((ghosts?.ghost_count ?? 0) === 0) console.log(import_chalk33.default.green(" no ghost tuples"));
5197
+ 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)`)}`);
5198
+ } else {
5199
+ const n = ghosts?.ghost_count ?? 0;
5200
+ 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)`)}`));
5201
+ else {
5202
+ console.log(import_chalk33.default.yellow(` ${n} ghost tuple(s) referencing entities absent from D1:`));
5203
+ for (const g of (ghosts.ghosts ?? []).slice(0, 20)) {
5204
+ console.log(import_chalk33.default.dim(` ${g.object_type}:${g.object_id} ${g.relation} ${g._user}`));
5205
+ }
5206
+ if (n > 20) console.log(import_chalk33.default.dim(` \u2026 and ${n - 20} more`));
5207
+ }
5208
+ }
5209
+ for (const e of ghosts?.errors ?? []) console.log(import_chalk33.default.red(` error: ${e}`));
5082
5210
  console.log();
5083
5211
  }
5084
5212
  async function runOp(sub, opts = {}) {
@@ -5096,8 +5224,9 @@ async function runOp(sub, opts = {}) {
5096
5224
  console.log(import_chalk33.default.bold("\nOperator menu"));
5097
5225
  console.log(` ${import_chalk33.default.cyan("apiblaze op residue")} external-store residue report (Upstash + Neon/OpenFGA, dry-run)`);
5098
5226
  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
- `);
5227
+ console.log(` ${import_chalk33.default.cyan("apiblaze op credits")} list credit wallets`);
5228
+ console.log(import_chalk33.default.dim(` (to prune all non-CP data: run scripts/nuke-but-cp.sh --apply --sweep in the repo)
5229
+ `));
5101
5230
  return;
5102
5231
  }
5103
5232
  case "residue": {
@@ -5110,15 +5239,20 @@ async function runOp(sub, opts = {}) {
5110
5239
  const report = await opCall({ method: "GET", path: "/operator/external-residue", summary: "external residue report" });
5111
5240
  const nUp = report?.upstash?.orphans?.length ?? 0;
5112
5241
  const nFga = report?.fga?.orphans?.length ?? 0;
5242
+ const nGhost = report?.ghosts?.ghost_count ?? 0;
5243
+ const st = report?.fga?.side_tables ?? {};
5244
+ const nSide = (st.soft_deleted_stores ?? 0) + (st.orphan_models ?? 0) + (st.orphan_changelog ?? 0);
5113
5245
  printResidue(report, false);
5114
- if (nUp + nFga === 0) {
5246
+ if (nUp + nFga + nGhost + nSide === 0) {
5115
5247
  console.log(import_chalk33.default.green("Nothing to sweep."));
5116
5248
  return;
5117
5249
  }
5118
5250
  if (!opts.yes) {
5119
5251
  const readline2 = await import("readline/promises");
5120
5252
  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: `));
5253
+ const answer = await rl.question(
5254
+ 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: `)
5255
+ );
5122
5256
  rl.close();
5123
5257
  if (answer.trim() !== "sweep") return void console.log(import_chalk33.default.dim("Aborted."));
5124
5258
  }
@@ -5145,6 +5279,7 @@ async function runOp(sub, opts = {}) {
5145
5279
 
5146
5280
  // src/index.ts
5147
5281
  init_trace();
5282
+ init_auth();
5148
5283
  var program = new import_commander.Command();
5149
5284
  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
5285
  program.hook("preAction", () => {
@@ -5158,7 +5293,7 @@ function action(fn) {
5158
5293
  renderTrace();
5159
5294
  } catch (err) {
5160
5295
  renderTrace();
5161
- printError(err);
5296
+ await printError(err);
5162
5297
  process.exit(1);
5163
5298
  }
5164
5299
  };
@@ -5167,7 +5302,7 @@ program.command("login").description("Authenticate with APIblaze").action(async
5167
5302
  try {
5168
5303
  await runLogin();
5169
5304
  } catch (err) {
5170
- printError(err);
5305
+ await printError(err);
5171
5306
  process.exit(1);
5172
5307
  }
5173
5308
  });
@@ -5175,7 +5310,7 @@ program.command("create").description("Create a new API proxy (no login needed \
5175
5310
  try {
5176
5311
  await runCreate(opts);
5177
5312
  } catch (err) {
5178
- printError(err);
5313
+ await printError(err);
5179
5314
  process.exit(1);
5180
5315
  }
5181
5316
  });
@@ -5200,7 +5335,7 @@ program.command("dev").description("Put your localhost behind a public URL (dev
5200
5335
  }
5201
5336
  await runDev({ port: resolved, captureFile: opts.captureFile });
5202
5337
  } catch (err) {
5203
- printError(err);
5338
+ await printError(err);
5204
5339
  process.exit(1);
5205
5340
  }
5206
5341
  });
@@ -5213,7 +5348,7 @@ program.command("whoami").description("Show who you are \u2014 both API Producer
5213
5348
  try {
5214
5349
  await runWhoami(opts);
5215
5350
  } catch (err) {
5216
- printError(err);
5351
+ await printError(err);
5217
5352
  process.exit(1);
5218
5353
  }
5219
5354
  });
@@ -5221,7 +5356,7 @@ program.command("claim").description("Claim your anonymous workspace into your a
5221
5356
  try {
5222
5357
  await runClaim(code, opts);
5223
5358
  } catch (err) {
5224
- printError(err);
5359
+ await printError(err);
5225
5360
  process.exit(1);
5226
5361
  }
5227
5362
  });
@@ -5229,7 +5364,7 @@ program.command("team").description("Switch the active team").argument("[team]",
5229
5364
  try {
5230
5365
  await runTeam(team);
5231
5366
  } catch (err) {
5232
- printError(err);
5367
+ await printError(err);
5233
5368
  process.exit(1);
5234
5369
  }
5235
5370
  });
@@ -5237,7 +5372,7 @@ program.command("projects").description("List the projects in your team").action
5237
5372
  try {
5238
5373
  await runProjects();
5239
5374
  } catch (err) {
5240
- printError(err);
5375
+ await printError(err);
5241
5376
  process.exit(1);
5242
5377
  }
5243
5378
  });
@@ -5257,7 +5392,7 @@ var tenant = program.command("tenant").description("Manage tenants \u2014 bare c
5257
5392
  tenant.command("manage").description("Browse & edit one tenant: settings, login app clients, providers, issuers (search-first picker)").argument("[query]", "Search by tenant name/display name (omit to use your tenant scope or pick)").option("--tenant <slug>", "Exact tenant slug (skips the picker)").option("--team <id|name>", "Team (defaults to active team)").action(action((query, opts) => runTenantManage(query, opts)));
5258
5393
  tenant.command("use").description("Set the sticky tenant scope for future commands (search-first; --clear to unset)").argument("[query]", "Search by tenant name/display name").option("--clear", "Clear the tenant scope").option("--team <id|name>", "Team (defaults to active team)").action(action((query, opts) => runTenantUse(query, opts)));
5259
5394
  tenant.command("list").description("List tenants in your team (--q searches server-side)").option("--q <search>", "Filter by tenant name or display name").option("--limit <n>", "Page size (max 200)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantList(opts)));
5260
- tenant.command("create").description("Create a tenant in your team").requiredOption("--name <display>", "Display name").option("--slug <tenant_name>", "Explicit tenant slug (generated if omitted)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantCreate(opts)));
5395
+ tenant.command("create").description("Create a tenant in your team (tenant names are lowercase alphanumeric and globally unique)").requiredOption("--name <name>", "Tenant name \u2014 becomes the slug ({name}.portal.apiblaze.com) unless --slug overrides; also the display label").option("--slug <tenant_name>", "Explicit tenant slug when it should differ from --name").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantCreate(opts)));
5261
5396
  tenant.command("attach").description("Attach a tenant to a proxy").argument("<project>", "Project name or id").requiredOption("--tenant <slug>", "Tenant slug to attach").option("--auth-config <id>", "Auth config id to bind").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTenantAttach(project, opts)));
5262
5397
  tenant.command("delete").description("Delete a tenant (full cascade)").argument("<slug>", "Tenant slug to delete").option("--team <id|name>", "Team (defaults to active team)").option("-y, --yes", "Skip the confirmation prompt").action(action((slug, opts) => runTenantDelete(slug, opts)));
5263
5398
  tenant.command("cors").description("Set the CORS allow-list for a tenant").requiredOption("--tenant <slug>", "Tenant slug").option("--origins <list>", 'Comma-separated origins (or "*"); empty clears').option("--team <id|name>", "Team (defaults to active team)").action(action((opts) => runTenantCors(opts)));
@@ -5319,14 +5454,32 @@ Examples:
5319
5454
  $ npx apiblaze throttle myapi --rate 50 --verbose # configure + show the API call
5320
5455
  $ npx apiblaze consumer login # act as a consumer of your API
5321
5456
  `);
5322
- function printError(err) {
5457
+ async function recoverStaleTeam() {
5458
+ try {
5459
+ const creds = loadCredentials();
5460
+ if (!creds?.teamId) return;
5461
+ const { resolveLinkedTeam: resolveLinkedTeam2 } = await Promise.resolve().then(() => (init_team(), team_exports));
5462
+ const linked = await resolveLinkedTeam2({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
5463
+ if (!linked) {
5464
+ console.error(import_chalk34.default.yellow("Your account has no teams anymore (deleted?). Run `apiblaze login` or `apiblaze create` to get a workspace."));
5465
+ return;
5466
+ }
5467
+ if (linked.teamId === creds.teamId) return;
5468
+ const next = { ...creds, teamId: linked.teamId, teamName: linked.teamName };
5469
+ delete next.activeTenant;
5470
+ saveCredentials(next);
5471
+ 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.`));
5472
+ } catch {
5473
+ }
5474
+ }
5475
+ async function printError(err) {
5323
5476
  if (err instanceof ApiError) {
5324
5477
  const data = err.body;
5325
5478
  const extra = [data?.body?.reason, data?.body?.details, data?.details, data?.body?.error].find((x) => typeof x === "string" && x && x !== err.message);
5326
5479
  console.error(import_chalk34.default.red(`
5327
5480
  API error (${err.status}): ${err.message}${extra ? ` \u2014 ${extra}` : ""}`));
5328
5481
  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`."));
5482
+ await recoverStaleTeam();
5330
5483
  }
5331
5484
  } else if (err instanceof Error) {
5332
5485
  console.error(import_chalk34.default.red(`