apiblaze 0.15.0 → 0.15.2

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