apiblaze 0.14.0 → 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 +606 -564
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -370,6 +370,354 @@ var init_anon_cred = __esm({
370
370
  }
371
371
  });
372
372
 
373
+ // src/commands/create.ts
374
+ var create_exports = {};
375
+ __export(create_exports, {
376
+ buildTryItCurl: () => buildTryItCurl,
377
+ runCreate: () => runCreate
378
+ });
379
+ function normalizeName(raw) {
380
+ return (raw || "").toLowerCase().replace(/[^a-z0-9]/g, "");
381
+ }
382
+ function isHttpUrl(s) {
383
+ try {
384
+ const u = new URL((s || "").trim());
385
+ return u.protocol === "http:" || u.protocol === "https:";
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
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;
400
+ }
401
+ }
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}"`;
410
+ }
411
+ if (authType === "none") return `curl ${url}`;
412
+ return null;
413
+ }
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
+ }
424
+ }
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.`);
447
+ }
448
+ teamId = match.teamId;
449
+ }
450
+ }
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
+
373
721
  // src/lib/trace.ts
374
722
  function setVerbose(v) {
375
723
  verbose = v;
@@ -529,22 +877,39 @@ async function pickTenant(teamId, opts = {}) {
529
877
  q = String(nq ?? "").trim();
530
878
  continue;
531
879
  }
532
- return picked;
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
+ }
533
911
  }
534
912
  }
535
- async function createTenantInline(teamId) {
536
- const { default: inquirer2 } = await import("inquirer");
537
- const { display } = await inquirer2.prompt([{ type: "input", name: "display", message: "Display name for the new tenant:", validate: (s) => !!s.trim() || "required" }]);
538
- const out = await admin({
539
- method: "POST",
540
- path: `/teams/${encodeURIComponent(teamId)}/tenants`,
541
- body: { display_name: display.trim() },
542
- summary: `Create tenant "${display.trim()}"`
543
- });
544
- const slug = out?.tenant_name ?? out?.tenant?.tenant_name;
545
- console.log(import_chalk22.default.green(` Tenant ${import_chalk22.default.bold(slug)} created.`));
546
- return slug;
547
- }
548
913
  var import_chalk22, import_ora8, PAGE;
549
914
  var init_tenant_pick = __esm({
550
915
  "src/lib/tenant-pick.ts"() {
@@ -562,7 +927,7 @@ var import_commander = require("commander");
562
927
  var import_chalk34 = __toESM(require("chalk"));
563
928
 
564
929
  // package.json
565
- var version = "0.14.0";
930
+ var version = "0.15.0";
566
931
 
567
932
  // src/index.ts
568
933
  init_types();
@@ -1217,590 +1582,256 @@ async function offerAutoCreate(teamId, port) {
1217
1582
  console.log(import_chalk4.default.dim(" Send it as the X-API-Key header. It may not be shown again."));
1218
1583
  }
1219
1584
  }
1220
- const targets = await getLocalhostTargets(teamId).catch(() => []);
1221
- const created = targets.find((t) => t.projectId === result.project_id);
1222
- if (!created) {
1223
- console.log(import_chalk4.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
1224
- return null;
1225
- }
1226
- return created;
1227
- }
1228
- function isInternalTarget(url) {
1229
- if (!url) return false;
1230
- try {
1231
- const h = new URL(url).hostname.toLowerCase();
1232
- 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);
1233
- } catch {
1234
- return false;
1235
- }
1236
- }
1237
- function printTunnelEndpoints(restore, targets) {
1238
- if (restore.length === 0) return;
1239
- console.log(import_chalk4.default.bold("\nYour proxy is live at:"));
1240
- for (const r of restore) {
1241
- const label2 = targets.find((t) => t.projectId === r.projectId)?.projectName ?? r.projectId;
1242
- console.log(`
1243
- ${import_chalk4.default.bold(label2)}`);
1244
- const internalEnvs = Object.keys(r.environments ?? {}).filter((e) => isInternalTarget(r.environments[e]?.target));
1245
- const envs = internalEnvs.includes("dev") ? ["dev"] : internalEnvs.length ? internalEnvs : ["dev"];
1246
- for (const env of envs) {
1247
- console.log(` ${import_chalk4.default.dim("API: ")} ${import_chalk4.default.cyan(`https://${r.projectId}.abz.run/${r.apiVersion}/${env}/`)}`);
1248
- }
1249
- if (r.tenant) {
1250
- console.log(` ${import_chalk4.default.dim("Portal:")} ${import_chalk4.default.cyan(`https://${r.tenant}.portal.apiblaze.com/${r.apiVersion}`)}`);
1251
- }
1252
- }
1253
- }
1254
- async function probeLocalServer(port) {
1255
- const controller = new AbortController();
1256
- const timer = setTimeout(() => controller.abort(), 1500);
1257
- try {
1258
- await fetch(`http://127.0.0.1:${port}/`, { method: "HEAD", signal: controller.signal });
1259
- return true;
1260
- } catch (err) {
1261
- if (err?.name === "AbortError") return true;
1262
- const code = err?.cause?.code;
1263
- return !(code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH");
1264
- } finally {
1265
- clearTimeout(timer);
1266
- }
1267
- }
1268
- async function runDev(options) {
1269
- const creds = await ensureLoggedIn(!!process.stdin.isTTY);
1270
- const linked = await resolveLinkedTeam({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
1271
- if (!linked) {
1272
- console.error(import_chalk4.default.red("No team available. Run `apiblaze login` to set up your team."));
1273
- process.exit(1);
1274
- }
1275
- const teamId = linked.teamId;
1276
- if (linked.teamId !== creds.teamId || linked.teamName !== creds.teamName) {
1277
- saveCredentials({ ...creds, teamId: linked.teamId, teamName: linked.teamName });
1278
- }
1279
- if (linked.teamName) {
1280
- console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(linked.teamName)}`);
1281
- }
1282
- let targets;
1283
- {
1284
- const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
1285
- try {
1286
- targets = await getLocalhostTargets(teamId);
1287
- spinner.stop();
1288
- } catch (err) {
1289
- spinner.fail("Failed to fetch projects.");
1290
- throw err;
1291
- }
1292
- }
1293
- let selectedTargets;
1294
- if (targets.length === 0) {
1295
- const created = await offerAutoCreate(teamId, options.port);
1296
- if (!created) {
1297
- console.log("Set a project's upstream target to localhost or a private IP, then try again.");
1298
- process.exit(0);
1299
- }
1300
- selectedTargets = [created];
1301
- } else if (targets.length === 1) {
1302
- const { confirmed } = await import_inquirer.default.prompt([{
1303
- type: "confirm",
1304
- name: "confirmed",
1305
- message: `Found 1 project with an internal target \u2014 tunnel "${import_chalk4.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
1306
- default: true
1307
- }]);
1308
- if (!confirmed) {
1309
- console.log("Aborted.");
1310
- process.exit(0);
1311
- }
1312
- selectedTargets = targets;
1313
- } else {
1314
- const ALL = "__all__";
1315
- const { chosen } = await import_inquirer.default.prompt([{
1316
- type: "list",
1317
- name: "chosen",
1318
- message: `Found ${targets.length} projects with an internal target \u2014 pick one to tunnel:`,
1319
- choices: [
1320
- ...targets.map((t) => ({
1321
- name: `${import_chalk4.default.bold(t.projectName)} (${t.tenantName}) \u2014 ${t.target}`,
1322
- value: t
1323
- })),
1324
- new import_inquirer.default.Separator(),
1325
- { name: `Tunnel all ${targets.length}`, value: ALL }
1326
- ]
1327
- }]);
1328
- selectedTargets = chosen === ALL ? targets : [chosen];
1329
- }
1330
- console.log(
1331
- import_chalk4.default.green(`
1332
- Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
1333
- `)
1334
- );
1335
- let recordSink;
1336
- let captureStream;
1337
- if (options.captureFile) {
1338
- captureStream = import_fs.default.createWriteStream(options.captureFile, { flags: "a" });
1339
- recordSink = (r) => captureStream.write(JSON.stringify(r) + "\n");
1340
- console.log(import_chalk4.default.gray(`Streaming full traffic to ${options.captureFile}
1341
- `));
1342
- }
1343
- let restore = [];
1344
- let connect;
1345
- {
1346
- const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
1347
- try {
1348
- const result = await putDevTunnel({
1349
- targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
1350
- });
1351
- restore = result.restore ?? [];
1352
- connect = result.connect;
1353
- spinner.succeed("Tunnel registered.");
1354
- } catch (err) {
1355
- spinner.fail("Failed to register tunnel.");
1356
- throw err;
1357
- }
1358
- }
1359
- printTunnelEndpoints(restore, selectedTargets);
1360
- const clients = connect.projects.map(
1361
- (projectId) => startTunnelClient({
1362
- connectUrl: connect.url,
1363
- token: connect.token,
1364
- projectId,
1365
- localPort: options.port,
1366
- onEntry: (entry) => console.log(formatLogLine(entry)),
1367
- onStatus: (status) => console.log(import_chalk4.default.gray(`[${projectId}] ${status}`)),
1368
- onCapture: (req, note) => console.log(formatCapturedRequest(req, note)),
1369
- onCaptureStart: () => console.log(
1370
- import_chalk4.default.magenta(`
1371
- \u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
1372
- `)
1373
- ),
1374
- onResume: () => console.log(
1375
- import_chalk4.default.green(`
1376
- \u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
1377
- `)
1378
- ),
1379
- onRecord: recordSink
1380
- })
1381
- );
1382
- const localUp = await probeLocalServer(options.port);
1383
- console.log("\n" + import_chalk4.default.gray("\u2500".repeat(60)));
1384
- console.log(import_chalk4.default.bold("Live traffic") + import_chalk4.default.gray(" (Ctrl+C to stop)"));
1385
- console.log(
1386
- 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.`)
1387
- );
1388
- console.log(import_chalk4.default.gray("\u2500".repeat(60)) + "\n");
1389
- let isCleaningUp = false;
1390
- async function cleanup() {
1391
- if (isCleaningUp) return;
1392
- isCleaningUp = true;
1393
- console.log(import_chalk4.default.gray("\n\nShutting down..."));
1394
- for (const client of clients) client.close();
1395
- captureStream?.end();
1396
- await deleteDevTunnel(restore).catch(() => {
1397
- });
1398
- console.log(import_chalk4.default.green("Tunnel stopped."));
1399
- process.exit(0);
1400
- }
1401
- process.on("SIGINT", () => void cleanup());
1402
- process.on("SIGTERM", () => void cleanup());
1403
- await new Promise(() => {
1404
- });
1405
- }
1406
-
1407
- // src/commands/projects.ts
1408
- var import_chalk5 = __toESM(require("chalk"));
1409
- var import_ora3 = __toESM(require("ora"));
1410
- init_auth();
1411
- init_api();
1412
- async function runProjects() {
1413
- const creds = loadCredentials();
1414
- if (!creds) {
1415
- console.error(import_chalk5.default.red("Not logged in. Run `apiblaze login` first."));
1416
- process.exit(1);
1417
- }
1418
- if (creds.githubHandle) {
1419
- console.log(`${import_chalk5.default.cyan("\u2192")} Logged in as ${import_chalk5.default.bold("@" + creds.githubHandle)}`);
1420
- }
1421
- let teamId = creds.teamId;
1422
- let teamName = creds.teamName;
1423
- if (!teamId) {
1424
- const teams = await getTeams().catch(() => []);
1425
- if (teams.length === 1) {
1426
- teamId = teams[0].teamId;
1427
- teamName = teams[0].name;
1428
- } else if (teams.length > 1) {
1429
- const { default: inquirer2 } = await import("inquirer");
1430
- const { chosen } = await inquirer2.prompt([{
1431
- type: "list",
1432
- name: "chosen",
1433
- message: "Which team do you want to use?",
1434
- choices: teams.map((t) => ({ name: t.name, value: t.teamId }))
1435
- }]);
1436
- teamId = chosen;
1437
- teamName = teams.find((t) => t.teamId === chosen)?.name;
1438
- }
1439
- }
1440
- if (!teamId) {
1441
- console.error(import_chalk5.default.red("No team found. Run `apiblaze login` to set up your team."));
1442
- process.exit(1);
1443
- }
1444
- console.log(`${import_chalk5.default.cyan("\u2192")} Team: ${import_chalk5.default.bold(teamName ?? teamId)}
1445
- `);
1446
- const spinner = (0, import_ora3.default)("Fetching projects...").start();
1447
- let projects;
1448
- try {
1449
- projects = await getProjects(teamId);
1450
- spinner.stop();
1451
- } catch (err) {
1452
- spinner.fail("Failed to fetch projects.");
1453
- throw err;
1454
- }
1455
- if (projects.length === 0) {
1456
- console.log(import_chalk5.default.yellow("No projects found for this team."));
1457
- return;
1458
- }
1459
- const width = Math.max(...projects.map((p) => p.projectName.length));
1460
- for (const p of projects) {
1461
- console.log(` ${import_chalk5.default.bold(p.projectName.padEnd(width))} ${import_chalk5.default.dim("v" + p.apiVersion)}`);
1462
- }
1463
- console.log(import_chalk5.default.dim(`
1464
- ${projects.length} project${projects.length === 1 ? "" : "s"}`));
1465
- }
1466
-
1467
- // src/commands/create.ts
1468
- var import_fs2 = __toESM(require("fs"));
1469
- var import_chalk6 = __toESM(require("chalk"));
1470
- var import_ora4 = __toESM(require("ora"));
1471
- init_auth();
1472
- init_api();
1473
- function normalizeName(raw) {
1474
- return (raw || "").toLowerCase().replace(/[^a-z0-9]/g, "");
1475
- }
1476
- function isHttpUrl(s) {
1477
- try {
1478
- const u = new URL((s || "").trim());
1479
- return u.protocol === "http:" || u.protocol === "https:";
1480
- } catch {
1481
- return false;
1585
+ const targets = await getLocalhostTargets(teamId).catch(() => []);
1586
+ const created = targets.find((t) => t.projectId === result.project_id);
1587
+ if (!created) {
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;
1482
1590
  }
1591
+ return created;
1483
1592
  }
1484
- function stripTenantFromPortal(devPortal) {
1593
+ function isInternalTarget(url) {
1594
+ if (!url) return false;
1485
1595
  try {
1486
- const u = new URL(devPortal);
1487
- const dot = u.hostname.indexOf(".");
1488
- if (dot < 0) return devPortal;
1489
- const product = u.hostname.slice(0, dot).split("-")[0];
1490
- u.hostname = `${product}${u.hostname.slice(dot)}`;
1491
- 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);
1492
1598
  } catch {
1493
- return devPortal;
1599
+ return false;
1494
1600
  }
1495
1601
  }
1496
- function fail(message) {
1497
- console.error(import_chalk6.default.red(`Error: ${message}`));
1498
- process.exit(1);
1499
- }
1500
- function buildTryItCurl(url, authType, apiKey) {
1501
- if (authType === "api_key") {
1502
- if (!apiKey) return null;
1503
- 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
+ }
1504
1617
  }
1505
- if (authType === "none") return `curl ${url}`;
1506
- return null;
1507
1618
  }
1508
- function printCurlExample(url, authType, apiKey, devPortal) {
1509
- const curl = buildTryItCurl(url, authType, apiKey);
1510
- console.log();
1511
- if (curl) {
1512
- console.log(` ${import_chalk6.default.dim("Try it \u2014 copy/paste:")}`);
1513
- console.log(` ${import_chalk6.default.cyan(curl)}`);
1514
- } else if (authType === "oauth") {
1515
- 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,`);
1516
- 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);
1517
1631
  }
1518
1632
  }
1519
- var VALID_AUTH = ["api_key", "none", "oauth"];
1520
- async function runCreate(opts = {}) {
1521
- const creds = loadCredentials();
1522
- if (!creds) {
1523
- await runAnonymousCreate(opts);
1524
- 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);
1525
1639
  }
1526
- const interactive = !!process.stdin.isTTY && !opts.json;
1527
- const auth = (opts.auth ?? "api_key").toLowerCase();
1528
- if (!VALID_AUTH.includes(auth)) {
1529
- 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 });
1530
1643
  }
1531
- let teamId = creds.teamId;
1532
- if (opts.team) {
1533
- if (opts.team.startsWith("team_")) {
1534
- teamId = opts.team;
1535
- } else {
1536
- const teams = await getTeams().catch(() => []);
1537
- const match = teams.find(
1538
- (t) => t.teamId === opts.team || t.name.toLowerCase() === opts.team.toLowerCase()
1539
- );
1540
- if (!match) {
1541
- fail(`Team "${opts.team}" not found. Run \`apiblaze team\` to see your teams.`);
1542
- }
1543
- teamId = match.teamId;
1544
- }
1644
+ if (linked.teamName) {
1645
+ console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(linked.teamName)}`);
1545
1646
  }
1546
- if (!opts.json) console.log(import_chalk6.default.bold("\nCreate an API proxy\n"));
1547
- let name = "";
1548
- if (opts.name !== void 0) {
1549
- name = normalizeName(opts.name);
1550
- if (name.length < 3) fail("Proxy name must be at least 3 characters (letters and digits only).");
1551
- const check = await checkProxyName(name, teamId, opts.apiversion).catch(() => null);
1552
- if (check && (!check.canUseProjectName || !check.canUseApiVersion)) {
1553
- fail(`Proxy name "${name}" is not available${check.message ? ` \u2014 ${check.message}` : ""}.`);
1554
- }
1555
- } else if (interactive) {
1556
- const { default: inquirer2 } = await import("inquirer");
1557
- for (; ; ) {
1558
- const { rawName } = await inquirer2.prompt([{
1559
- type: "input",
1560
- name: "rawName",
1561
- message: "Proxy name (your API will live at <name>.abz.run):",
1562
- transformer: (v) => normalizeName(v)
1563
- }]);
1564
- name = normalizeName(rawName);
1565
- if (name.length < 3) {
1566
- console.log(import_chalk6.default.yellow(" Name must be at least 3 characters (letters and digits only).\n"));
1567
- continue;
1568
- }
1569
- const spinner2 = (0, import_ora4.default)("Checking availability...").start();
1570
- try {
1571
- const check = await checkProxyName(name, teamId, opts.apiversion);
1572
- spinner2.stop();
1573
- if (!check.canUseProjectName || !check.canUseApiVersion) {
1574
- console.log(import_chalk6.default.yellow(` "${name}" is not available${check.message ? ` \u2014 ${check.message}` : ""}. Try another.
1575
- `));
1576
- continue;
1577
- }
1578
- } catch {
1579
- spinner2.stop();
1580
- console.log(import_chalk6.default.dim(" (could not verify availability; continuing)"));
1581
- }
1582
- console.log(`${import_chalk6.default.cyan("\u2192")} Your API will live at ${import_chalk6.default.bold(`https://${name}.abz.run`)}
1583
- `);
1584
- 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;
1585
1656
  }
1586
- } else {
1587
- fail("--name is required in non-interactive mode.");
1588
1657
  }
1589
- let targetUrl = "";
1590
- if (opts.target !== void 0) {
1591
- if (!isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
1592
- targetUrl = opts.target.trim();
1593
- } else if (interactive) {
1594
- const { default: inquirer2 } = await import("inquirer");
1595
- for (; ; ) {
1596
- const { url } = await inquirer2.prompt([{
1597
- type: "input",
1598
- name: "url",
1599
- message: "Target URL to forward requests to (e.g. https://httpbin.org):"
1600
- }]);
1601
- if (!isHttpUrl(url)) {
1602
- console.log(import_chalk6.default.yellow(" Enter a valid http(s) URL.\n"));
1603
- continue;
1604
- }
1605
- targetUrl = url.trim();
1606
- 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);
1607
1664
  }
1608
- } else {
1609
- fail("--target is required in non-interactive mode.");
1610
- }
1611
- if (interactive && !opts.yes) {
1612
- const { default: inquirer2 } = await import("inquirer");
1613
- console.log(`${import_chalk6.default.cyan("\u2192")} Auth: ${import_chalk6.default.bold(auth)}${auth === "api_key" ? " \u2014 consumers send an X-API-Key header" : ""}`);
1614
- const { ok } = await inquirer2.prompt([{
1665
+ selectedTargets = [created];
1666
+ } else if (targets.length === 1) {
1667
+ const { confirmed } = await import_inquirer.default.prompt([{
1615
1668
  type: "confirm",
1616
- name: "ok",
1617
- 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})?`,
1618
1671
  default: true
1619
1672
  }]);
1620
- if (!ok) {
1621
- console.log(import_chalk6.default.yellow("Cancelled."));
1622
- return;
1623
- }
1624
- }
1625
- const spinner = !opts.json ? (0, import_ora4.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
1626
- let result;
1627
- try {
1628
- result = await createProxy({ name, target_url: targetUrl, auth_type: auth, team_id: teamId, ...opts.apiversion ? { api_version: opts.apiversion } : {} });
1629
- spinner?.succeed(import_chalk6.default.green("Proxy created!"));
1630
- } catch (err) {
1631
- spinner?.fail("Failed to create proxy.");
1632
- throw err;
1633
- }
1634
- const version2 = result.api_version || "1.0.0";
1635
- const keys = result.api_keys ?? {};
1636
- const adminKey = keys.dev ?? Object.values(keys)[0];
1637
- const proxyUrl = `https://${name}.abz.run/${version2}/dev`;
1638
- const devPortal = result.devPortal ? stripTenantFromPortal(result.devPortal) : void 0;
1639
- if (opts.json) {
1640
- process.stdout.write(JSON.stringify({
1641
- project_id: result.project_id,
1642
- api_version: version2,
1643
- proxy_url: proxyUrl,
1644
- dev_portal: devPortal,
1645
- api_key: adminKey,
1646
- api_keys: keys,
1647
- team_id: teamId
1648
- }) + "\n");
1649
- return;
1650
- }
1651
- console.log();
1652
- console.log(` ${import_chalk6.default.dim("Proxy URL: ")} ${import_chalk6.default.bold(proxyUrl)}`);
1653
- if (devPortal) console.log(` ${import_chalk6.default.dim("Dev portal:")} ${import_chalk6.default.bold(devPortal)}`);
1654
- if (adminKey) {
1655
- console.log();
1656
- console.log(` ${import_chalk6.default.dim("Consumer admin API key (dev):")}`);
1657
- console.log(` ${import_chalk6.default.bold.green(adminKey)}`);
1658
- 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."));
1659
- const otherEnvs = Object.keys(keys).filter((e) => e !== "dev");
1660
- if (otherEnvs.length) {
1661
- 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);
1662
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];
1663
1694
  }
1664
- printCurlExample(proxyUrl, auth, adminKey, devPortal);
1665
- console.log();
1666
- }
1667
- async function runAnonymousCreate(opts) {
1668
- const interactive = !!process.stdin.isTTY && !opts.json;
1669
- if (!opts.json) {
1670
- console.log(import_chalk6.default.bold("\nCreate an API proxy"));
1671
- 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
+ `));
1672
1707
  }
1673
- let body = {};
1674
- if (opts.config) {
1675
- let raw = "";
1676
- try {
1677
- raw = import_fs2.default.readFileSync(opts.config, "utf8");
1678
- } catch {
1679
- fail(`Cannot read --config file: ${opts.config}`);
1680
- }
1681
- let parsed;
1708
+ let restore = [];
1709
+ let connect;
1710
+ {
1711
+ const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
1682
1712
  try {
1683
- parsed = JSON.parse(raw);
1684
- } catch {
1685
- 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;
1686
1722
  }
1687
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) fail("--config must be a JSON object.");
1688
- body = parsed;
1689
1723
  }
1690
- let name = opts.name !== void 0 ? normalizeName(opts.name) : typeof body.name === "string" ? body.name : void 0;
1691
- if (opts.name !== void 0 && name.length < 3) {
1692
- 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);
1693
1765
  }
1694
- if (opts.target && !isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
1695
- let target = opts.target?.trim() || (typeof body.target === "string" ? body.target : "") || (typeof body.target_url === "string" ? body.target_url : "");
1696
- const hasOtherSource = !!(body.openapi || body.github);
1697
- if (!target && !hasOtherSource) {
1698
- 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) {
1699
1794
  const { default: inquirer2 } = await import("inquirer");
1700
- if (name === void 0) {
1701
- const { rawName } = await inquirer2.prompt([{
1702
- type: "input",
1703
- name: "rawName",
1704
- message: "Proxy name (leave blank to auto-generate):",
1705
- transformer: (v) => normalizeName(v)
1706
- }]);
1707
- const n = normalizeName(rawName);
1708
- name = n.length >= 3 ? n : void 0;
1709
- }
1710
- for (; ; ) {
1711
- const { url } = await inquirer2.prompt([{
1712
- type: "input",
1713
- name: "url",
1714
- message: "Target URL to forward requests to (e.g. https://httpbin.org):"
1715
- }]);
1716
- if (!isHttpUrl(url)) {
1717
- console.log(import_chalk6.default.yellow(" Enter a valid http(s) URL.\n"));
1718
- continue;
1719
- }
1720
- target = url.trim();
1721
- break;
1722
- }
1723
- } else {
1724
- 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;
1725
1803
  }
1726
1804
  }
1727
- if (target) {
1728
- body.target = target;
1729
- body.target_url = target;
1730
- }
1731
- if (name) {
1732
- body.name = name;
1733
- 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);
1734
1808
  }
1735
- if (opts.subdomain) body.subdomain = normalizeName(opts.subdomain);
1736
- if (opts.tenant) body.tenant = normalizeName(opts.tenant);
1737
- if (opts.product) body.product_slug = normalizeName(opts.product);
1738
- if (opts.displayName) body.display_name = opts.displayName;
1739
- if (opts.apiversion) body.api_version = opts.apiversion;
1740
- if (opts.auth && opts.auth !== "api_key") body.auth_type = opts.auth;
1741
- const { loadAnonCred: loadAnonCred2, saveAnonCred: saveAnonCred2, clearAnonCred: clearAnonCred2, cpFetch: cpFetch2 } = await Promise.resolve().then(() => (init_anon_cred(), anon_cred_exports));
1742
- if (opts.newSession) clearAnonCred2();
1743
- const cred = loadAnonCred2();
1744
- const spinner = !opts.json ? (0, import_ora4.default)(cred ? "Creating proxy (in your anonymous workspace)..." : "Creating proxy...").start() : null;
1745
- 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;
1746
1813
  try {
1747
- if (cred) {
1748
- result = await cpFetch2(cred.cp_key, "/projects", { method: "POST", body: JSON.stringify(body) });
1749
- if (Array.isArray(result.endpoints)) {
1750
- result.endpoints = result.endpoints.map((e) => e.replace(/:\/\/[^/]+/, `://${result.project_id}.tryabz.run`));
1751
- }
1752
- } else {
1753
- result = await createProxyAnonymous(body);
1754
- if (result.cp_key && result.team_id) {
1755
- saveAnonCred2(result.cp_key, result.team_id, result.claim_code);
1756
- }
1757
- }
1758
- spinner?.succeed(import_chalk6.default.green("Proxy created!"));
1814
+ projects = await getProjects(teamId);
1815
+ spinner.stop();
1759
1816
  } catch (err) {
1760
- spinner?.fail("Failed to create proxy.");
1817
+ spinner.fail("Failed to fetch projects.");
1761
1818
  throw err;
1762
1819
  }
1763
- const version2 = result.api_version || "1.0.0";
1764
- const keys = result.api_keys ?? {};
1765
- const apiKey = result.apiKey ?? keys.prod ?? Object.values(keys)[0];
1766
- const prodEndpoint = (result.endpoints || []).find((e) => e.endsWith("/prod")) || (result.endpoints || [])[0];
1767
- if (opts.json) {
1768
- process.stdout.write(JSON.stringify({
1769
- project_id: result.project_id,
1770
- api_version: version2,
1771
- endpoints: result.endpoints,
1772
- api_key: apiKey,
1773
- api_keys: keys,
1774
- claim_url: result.claim_url,
1775
- anonymous: true
1776
- }) + "\n");
1820
+ if (projects.length === 0) {
1821
+ console.log(import_chalk5.default.yellow("No projects found for this team."));
1777
1822
  return;
1778
1823
  }
1779
- console.log();
1780
- if (prodEndpoint) console.log(` ${import_chalk6.default.dim("Proxy URL: ")} ${import_chalk6.default.bold(prodEndpoint)}`);
1781
- if (result.portal) console.log(` ${import_chalk6.default.dim("Dev portal:")} ${import_chalk6.default.bold(result.portal)}`);
1782
- if (apiKey) {
1783
- console.log();
1784
- console.log(` ${import_chalk6.default.dim("API key:")}`);
1785
- console.log(` ${import_chalk6.default.bold.green(apiKey)}`);
1786
- 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."));
1787
- }
1788
- if (prodEndpoint) printCurlExample(prodEndpoint, opts.auth || "api_key", apiKey, result.portal);
1789
- const claimCode = result.claim_code || cred?.claim_code;
1790
- if (claimCode) {
1791
- console.log();
1792
- console.log(` ${import_chalk6.default.yellow("\u26A0 Anonymous \u2014 claim within 30 days or it expires.")} Everything you create`);
1793
- console.log(` with the CP key shares ONE workspace. To keep it all in one shot:`);
1794
- console.log(` ${import_chalk6.default.cyan("apiblaze login")} ${import_chalk6.default.dim("(prompts to claim your workspace into your account)")}`);
1795
- console.log(import_chalk6.default.dim(` From another machine: apiblaze claim ${claimCode} (add --team <name> to merge into an existing team)`));
1796
- } else if (result.claim_url) {
1797
- console.log();
1798
- console.log(` ${import_chalk6.default.yellow("\u26A0 Anonymous proxy \u2014 claim it to your account within 30 days or it expires:")}`);
1799
- 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)}`);
1800
1827
  }
1801
- console.log();
1828
+ console.log(import_chalk5.default.dim(`
1829
+ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
1802
1830
  }
1803
1831
 
1832
+ // src/index.ts
1833
+ init_create();
1834
+
1804
1835
  // src/commands/claim.ts
1805
1836
  var import_chalk7 = __toESM(require("chalk"));
1806
1837
  init_auth();
@@ -3135,10 +3166,20 @@ async function clientsMenu(teamId, tenant2, base) {
3135
3166
  }]);
3136
3167
  if (pick2 === " back") return;
3137
3168
  if (pick2 === " create") {
3138
- const projects = await getProjects(teamId).catch(() => []);
3169
+ let projects = await getProjects(teamId).catch(() => []);
3139
3170
  if (!projects.length) {
3140
- console.log(import_chalk24.default.yellow(" No projects in this team \u2014 create a proxy first."));
3141
- 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;
3142
3183
  }
3143
3184
  const a = await inquirer2.prompt([
3144
3185
  { type: "input", name: "name", message: "Client name:", validate: (s) => !!s.trim() || "required" },
@@ -3404,6 +3445,7 @@ init_auth();
3404
3445
  var import_chalk26 = __toESM(require("chalk"));
3405
3446
  init_admin();
3406
3447
  init_api();
3448
+ init_create();
3407
3449
  async function proj(teamId, name, version2) {
3408
3450
  return resolveProject(teamId, name, version2);
3409
3451
  }
@@ -5350,7 +5392,7 @@ var tenant = program.command("tenant").description("Manage tenants \u2014 bare c
5350
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)));
5351
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)));
5352
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)));
5353
- 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)));
5354
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)));
5355
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)));
5356
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)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "APIblaze CLI + sidecar — manage API proxies, run dev tunnels, and route a Next.js app's egress through APIblaze with one command",
5
5
  "keywords": [
6
6
  "apiblaze",