apiblaze 0.3.0 → 0.3.1

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 (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.js +389 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,6 +36,9 @@ npx apiblaze dev
36
36
  # Or specify a port
37
37
  npx apiblaze dev 3000
38
38
 
39
+ # Stream full request/response traffic to a file (JSON lines)
40
+ npx apiblaze dev 3000 --capture-file traffic.jsonl
41
+
39
42
  ```
40
43
 
41
44
  ## Help
@@ -70,6 +73,17 @@ apiblaze help dev
70
73
 
71
74
  On Ctrl+C the tunnel is cleanly deregistered.
72
75
 
76
+ ### Zero-setup conveniences
77
+
78
+ - **No project yet?** If none of your projects point at this machine, `apiblaze dev`
79
+ offers to spin up a throwaway dev proxy (random name like `braveotter42`) pointed at
80
+ `http://localhost:<port>` and tunnels it immediately — pick `none` or `api_key` auth.
81
+ - **Server not running yet?** If nothing is listening on the local port, requests aren't
82
+ dropped: each one is printed in full (headers + body, with API keys/JWTs masked and JWTs
83
+ decoded) and answered with a friendly synthetic `200`. The moment your dev server comes
84
+ up, the next request forwards to it automatically. Add `--capture-file <path>` to also
85
+ stream every request/response to a JSON-lines file.
86
+
73
87
  ## License
74
88
 
75
89
  MIT
package/dist/index.js CHANGED
@@ -221,7 +221,7 @@ var import_commander = require("commander");
221
221
  var import_chalk14 = __toESM(require("chalk"));
222
222
 
223
223
  // package.json
224
- var version = "0.3.0";
224
+ var version = "0.3.1";
225
225
 
226
226
  // src/index.ts
227
227
  init_types();
@@ -342,6 +342,7 @@ ${import_chalk.default.cyan("\u2192")} Team: ${import_chalk.default.bold(teamNam
342
342
  }
343
343
 
344
344
  // src/commands/dev.ts
345
+ var import_fs = __toESM(require("fs"));
345
346
  var import_chalk3 = __toESM(require("chalk"));
346
347
  var import_ora2 = __toESM(require("ora"));
347
348
  var import_inquirer = __toESM(require("inquirer"));
@@ -376,10 +377,217 @@ function colorLatency(latency) {
376
377
  if (latency < 500) return import_chalk2.default.yellow(s);
377
378
  return import_chalk2.default.red(s);
378
379
  }
380
+ function timestamp() {
381
+ return (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
382
+ }
379
383
  function formatLogLine(entry) {
380
- const now = /* @__PURE__ */ new Date();
381
- const ts = now.toTimeString().slice(0, 8);
382
- return `${import_chalk2.default.gray(`[${ts}]`)} ${colorMethod(entry.method)} ${import_chalk2.default.white(entry.path)} ${import_chalk2.default.gray("\u2192")} ${colorStatus(entry.status)} ${import_chalk2.default.gray(`(${colorLatency(entry.latency)})`)}`;
384
+ return `${import_chalk2.default.gray(`[${timestamp()}]`)} ${colorMethod(entry.method)} ${import_chalk2.default.white(entry.path)} ${import_chalk2.default.gray("\u2192")} ${colorStatus(entry.status)} ${import_chalk2.default.gray(`(${colorLatency(entry.latency)})`)}`;
385
+ }
386
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
387
+ "authorization",
388
+ "proxy-authorization",
389
+ "cookie",
390
+ "set-cookie",
391
+ "x-api-key",
392
+ "api-key",
393
+ "apikey",
394
+ "x-auth-token",
395
+ "x-access-token",
396
+ "x-refresh-token",
397
+ "x-amz-security-token",
398
+ "x-csrf-token"
399
+ ]);
400
+ var SENSITIVE_QUERY = /* @__PURE__ */ new Set([
401
+ "api_key",
402
+ "apikey",
403
+ "key",
404
+ "token",
405
+ "access_token",
406
+ "refresh_token",
407
+ "id_token",
408
+ "secret",
409
+ "client_secret",
410
+ "password",
411
+ "sig",
412
+ "signature"
413
+ ]);
414
+ function maskSecret(value) {
415
+ const v = value.trim();
416
+ if (v.length <= 8) return "\u2022\u2022\u2022\u2022";
417
+ const head = v.slice(0, 4);
418
+ const tail = v.slice(-4);
419
+ return `${head}\u2026${tail} ${import_chalk2.default.gray(`(${v.length} chars, masked)`)}`;
420
+ }
421
+ function base64UrlDecode(seg) {
422
+ try {
423
+ const pad = seg.length % 4 === 0 ? "" : "=".repeat(4 - seg.length % 4);
424
+ const b64 = seg.replace(/-/g, "+").replace(/_/g, "/") + pad;
425
+ return Buffer.from(b64, "base64").toString("utf8");
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
430
+ function decodeJwt(token) {
431
+ const parts = token.trim().split(".");
432
+ if (parts.length !== 3) return null;
433
+ const headerRaw = base64UrlDecode(parts[0]);
434
+ const payloadRaw = base64UrlDecode(parts[1]);
435
+ if (!headerRaw || !payloadRaw) return null;
436
+ try {
437
+ const header = JSON.parse(headerRaw);
438
+ const payload = JSON.parse(payloadRaw);
439
+ if (!header || typeof header !== "object" || !("alg" in header)) return null;
440
+ return { header, payload };
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+ function maskPath(path2) {
446
+ const q = path2.indexOf("?");
447
+ if (q < 0) return path2;
448
+ const base = path2.slice(0, q);
449
+ const query = path2.slice(q + 1);
450
+ const masked = query.split("&").map((pair) => {
451
+ const eq = pair.indexOf("=");
452
+ if (eq < 0) return pair;
453
+ const name = pair.slice(0, eq);
454
+ const val = pair.slice(eq + 1);
455
+ if (val && SENSITIVE_QUERY.has(decodeURIComponent(name).toLowerCase())) {
456
+ return `${name}=${maskSecret(decodeURIComponent(val))}`;
457
+ }
458
+ return pair;
459
+ }).join("&");
460
+ return `${base}?${masked}`;
461
+ }
462
+ function formatHeaderLines(name, value) {
463
+ const lower = name.toLowerCase();
464
+ if (lower === "authorization" || lower === "proxy-authorization") {
465
+ const m = /^Bearer\s+(.+)$/i.exec(value.trim());
466
+ if (m) {
467
+ const token = m[1];
468
+ const jwt = decodeJwt(token);
469
+ if (jwt) {
470
+ return [
471
+ ` ${import_chalk2.default.dim(name)}: Bearer ${maskSecret(token)} ${import_chalk2.default.cyan("(JWT)")}`,
472
+ ` ${import_chalk2.default.gray("\u251C header: ")} ${import_chalk2.default.gray(JSON.stringify(jwt.header))}`,
473
+ ` ${import_chalk2.default.gray("\u2514 payload:")} ${import_chalk2.default.gray(JSON.stringify(jwt.payload))}`
474
+ ];
475
+ }
476
+ return [` ${import_chalk2.default.dim(name)}: Bearer ${maskSecret(token)}`];
477
+ }
478
+ return [` ${import_chalk2.default.dim(name)}: ${maskSecret(value)}`];
479
+ }
480
+ if (SENSITIVE_HEADERS.has(lower)) {
481
+ return [` ${import_chalk2.default.dim(name)}: ${maskSecret(value)}`];
482
+ }
483
+ return [` ${import_chalk2.default.dim(name)}: ${value}`];
484
+ }
485
+ function formatBody(body, contentType) {
486
+ if (body.length === 0) return import_chalk2.default.gray(" (empty)");
487
+ if (body.includes(0)) return import_chalk2.default.gray(` <binary, ${body.length} bytes>`);
488
+ let text = body.toString("utf8");
489
+ const ct = (contentType ?? "").toLowerCase();
490
+ if (ct.includes("json") || /^[[{]/.test(text.trim())) {
491
+ try {
492
+ text = JSON.stringify(JSON.parse(text), null, 2);
493
+ } catch {
494
+ }
495
+ }
496
+ const MAX = 4e3;
497
+ const truncated = text.length > MAX;
498
+ const shown = truncated ? text.slice(0, MAX) : text;
499
+ const indented = shown.split("\n").map((l) => ` ${l}`).join("\n");
500
+ return indented + (truncated ? import_chalk2.default.gray(`
501
+ \u2026 (${text.length - MAX} more bytes \u2014 see --capture-file for full)`) : "");
502
+ }
503
+ function formatCapturedRequest(req, note) {
504
+ const headerLines = Object.keys(req.headers).flatMap((name) => formatHeaderLines(name, req.headers[name]));
505
+ const contentType = Object.keys(req.headers).find((k) => k.toLowerCase() === "content-type");
506
+ return [
507
+ `${import_chalk2.default.gray(`[${timestamp()}]`)} ${import_chalk2.default.magenta("\u26B2 CAPTURED")} ${colorMethod(req.method)} ${import_chalk2.default.white(maskPath(req.path))} ${import_chalk2.default.gray(`\u2014 ${note}`)}`,
508
+ import_chalk2.default.bold(" Headers:"),
509
+ ...headerLines,
510
+ import_chalk2.default.bold(" Body:"),
511
+ formatBody(req.body, contentType ? req.headers[contentType] : void 0),
512
+ ""
513
+ ].join("\n");
514
+ }
515
+
516
+ // src/lib/random-name.ts
517
+ var ADJECTIVES = [
518
+ "amber",
519
+ "azure",
520
+ "brave",
521
+ "bright",
522
+ "calm",
523
+ "clever",
524
+ "cosmic",
525
+ "crisp",
526
+ "eager",
527
+ "fancy",
528
+ "gentle",
529
+ "happy",
530
+ "jolly",
531
+ "keen",
532
+ "lively",
533
+ "lucky",
534
+ "mellow",
535
+ "merry",
536
+ "nimble",
537
+ "noble",
538
+ "plucky",
539
+ "quiet",
540
+ "rapid",
541
+ "shiny",
542
+ "smooth",
543
+ "snappy",
544
+ "sunny",
545
+ "swift",
546
+ "tidy",
547
+ "vivid",
548
+ "witty",
549
+ "zesty"
550
+ ];
551
+ var NOUNS = [
552
+ "badger",
553
+ "beacon",
554
+ "cedar",
555
+ "comet",
556
+ "dolphin",
557
+ "ember",
558
+ "falcon",
559
+ "finch",
560
+ "harbor",
561
+ "heron",
562
+ "koala",
563
+ "lark",
564
+ "lemur",
565
+ "maple",
566
+ "meadow",
567
+ "otter",
568
+ "panda",
569
+ "pebble",
570
+ "puffin",
571
+ "quail",
572
+ "raccoon",
573
+ "river",
574
+ "robin",
575
+ "sparrow",
576
+ "spruce",
577
+ "tiger",
578
+ "turtle",
579
+ "walrus",
580
+ "willow",
581
+ "wombat",
582
+ "yak",
583
+ "zebra"
584
+ ];
585
+ function pick(list) {
586
+ return list[Math.floor(Math.random() * list.length)];
587
+ }
588
+ function randomProxyName() {
589
+ const digits = String(Math.floor(Math.random() * 100)).padStart(2, "0");
590
+ return `${pick(ADJECTIVES)}${pick(NOUNS)}${digits}`;
383
591
  }
384
592
 
385
593
  // src/lib/tunnel-client.ts
@@ -387,6 +595,7 @@ var import_ws = __toESM(require("ws"));
387
595
  var CHUNK_BYTES = 512 * 1024;
388
596
  var PING_INTERVAL_MS = 6e4;
389
597
  var MAX_RECONNECT_DELAY_MS = 15e3;
598
+ var OFFLINE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EHOSTUNREACH"]);
390
599
  var STRIP_HEADERS = /* @__PURE__ */ new Set([
391
600
  "host",
392
601
  "content-length",
@@ -404,6 +613,10 @@ function stripHeaders(headers) {
404
613
  }
405
614
  return out;
406
615
  }
616
+ function encodeBody(buf) {
617
+ if (buf.includes(0)) return { value: buf.toString("base64"), encoding: "base64" };
618
+ return { value: buf.toString("utf8"), encoding: "utf8" };
619
+ }
407
620
  function startTunnelClient(opts) {
408
621
  const target = `http://127.0.0.1:${opts.localPort}`;
409
622
  const inflight = /* @__PURE__ */ new Map();
@@ -411,6 +624,7 @@ function startTunnelClient(opts) {
411
624
  let closed = false;
412
625
  let reconnects = 0;
413
626
  let pingTimer;
627
+ let capturing = false;
414
628
  function connect() {
415
629
  const url = `${opts.connectUrl}?project=${encodeURIComponent(opts.projectId)}&token=${encodeURIComponent(opts.token)}`;
416
630
  const socket = new import_ws.default(url);
@@ -470,30 +684,80 @@ function startTunnelClient(opts) {
470
684
  inflight.delete(id);
471
685
  void forward(socket, id, f);
472
686
  }
687
+ function sendResponse(socket, id, status, headers, body) {
688
+ send(socket, { id, type: "res", status, headers, bodyLen: body.length });
689
+ if (body.length === 0) {
690
+ send(socket, { id, type: "chunk", seq: 0, data: "", final: true });
691
+ return;
692
+ }
693
+ for (let off = 0, seq = 0; off < body.length; off += CHUNK_BYTES) {
694
+ const slice = body.subarray(off, Math.min(off + CHUNK_BYTES, body.length));
695
+ send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= body.length });
696
+ }
697
+ }
698
+ function record(f, reqBody, status, captured, resHeaders, resBody) {
699
+ if (!opts.onRecord) return;
700
+ const req = encodeBody(reqBody);
701
+ const res = resBody ? encodeBody(resBody) : void 0;
702
+ opts.onRecord({
703
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
704
+ projectId: opts.projectId,
705
+ captured,
706
+ method: f.method,
707
+ path: f.path,
708
+ requestHeaders: f.headers,
709
+ requestBody: reqBody.length ? req.value : void 0,
710
+ requestBodyEncoding: reqBody.length ? req.encoding : void 0,
711
+ status,
712
+ responseHeaders: resHeaders,
713
+ responseBody: res?.value,
714
+ responseBodyEncoding: res?.encoding,
715
+ latencyMs: Date.now() - f.start
716
+ });
717
+ }
473
718
  async function forward(socket, id, f) {
474
719
  const body = Buffer.concat(f.chunks);
475
720
  const init = { method: f.method, headers: stripHeaders(f.headers) };
476
721
  if (body.length) init.body = body;
477
- let status = 502;
478
722
  try {
479
723
  const resp = await fetch(target + f.path, init);
480
- status = resp.status;
724
+ const status = resp.status;
481
725
  const buf = Buffer.from(await resp.arrayBuffer());
482
726
  const headers = {};
483
727
  resp.headers.forEach((value, key) => {
484
728
  if (!STRIP_HEADERS.has(key.toLowerCase())) headers[key] = value;
485
729
  });
486
- send(socket, { id, type: "res", status, headers, bodyLen: buf.length });
487
- for (let off = 0, seq = 0; off < buf.length; off += CHUNK_BYTES) {
488
- const slice = buf.subarray(off, Math.min(off + CHUNK_BYTES, buf.length));
489
- send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= buf.length });
730
+ if (capturing) {
731
+ capturing = false;
732
+ opts.onResume?.();
490
733
  }
734
+ sendResponse(socket, id, status, headers, buf);
735
+ opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
736
+ record(f, body, status, false, headers, buf);
491
737
  } catch (err) {
492
738
  const code = err?.cause?.code;
493
- const message = code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ENOTFOUND" || code === "EHOSTUNREACH" ? `No local server reachable at ${target} \u2014 is your dev server running on port ${opts.localPort}?` : err?.cause?.message || err?.message || String(err);
739
+ if (OFFLINE_CODES.has(code)) {
740
+ if (!capturing) {
741
+ capturing = true;
742
+ opts.onCaptureStart?.();
743
+ }
744
+ const note = `no local server on port ${opts.localPort}`;
745
+ const payload = Buffer.from(JSON.stringify({
746
+ apiblaze_dev: "captured",
747
+ message: `No local server on port ${opts.localPort} \u2014 request captured by \`apiblaze dev\`. Start your server and resend to forward it.`,
748
+ request: { method: f.method, path: f.path }
749
+ }, null, 2));
750
+ const headers = { "content-type": "application/json", "x-apiblaze-dev": "captured" };
751
+ sendResponse(socket, id, 200, headers, payload);
752
+ opts.onCapture?.({ method: f.method, path: f.path, headers: f.headers, body }, note);
753
+ record(f, body, 200, true, headers, payload);
754
+ return;
755
+ }
756
+ const message = err?.cause?.message || err?.message || String(err);
494
757
  send(socket, { id, type: "err", message });
758
+ opts.onEntry({ method: f.method, path: f.path, status: 502, latency: Date.now() - f.start });
759
+ record(f, body, 502, false);
495
760
  }
496
- opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
497
761
  }
498
762
  function send(socket, frame) {
499
763
  try {
@@ -515,6 +779,76 @@ function startTunnelClient(opts) {
515
779
  }
516
780
 
517
781
  // src/commands/dev.ts
782
+ async function offerAutoCreate(teamId, port) {
783
+ if (!process.stdin.isTTY) {
784
+ console.log(import_chalk3.default.yellow("No projects found with an internal target."));
785
+ console.log("Point a project at localhost in the dashboard, or run `apiblaze dev` in an interactive terminal to create one automatically.");
786
+ return null;
787
+ }
788
+ const { create } = await import_inquirer.default.prompt([{
789
+ type: "confirm",
790
+ name: "create",
791
+ message: `No project points at this machine. Create a quick dev proxy \u2192 ${import_chalk3.default.bold(`http://localhost:${port}`)} and tunnel it?`,
792
+ default: true
793
+ }]);
794
+ if (!create) return null;
795
+ const { auth } = await import_inquirer.default.prompt([{
796
+ type: "list",
797
+ name: "auth",
798
+ message: "How should callers authenticate to the new proxy?",
799
+ choices: [
800
+ { name: "none \u2014 open to anyone with the URL (simplest)", value: "none" },
801
+ { name: "api_key \u2014 callers must send an X-API-Key header", value: "api_key" }
802
+ ],
803
+ default: "none"
804
+ }]);
805
+ let name = randomProxyName();
806
+ for (let i = 0; i < 8; i++) {
807
+ const check = await checkProxyName(name, teamId).catch(() => null);
808
+ if (!check || check.canUseProjectName && check.canUseApiVersion) break;
809
+ name = randomProxyName();
810
+ }
811
+ const spinner = (0, import_ora2.default)(`Creating dev proxy "${name}"...`).start();
812
+ let result;
813
+ try {
814
+ result = await createProxy({ name, target_url: `http://localhost:${port}`, auth_type: auth, team_id: teamId });
815
+ spinner.succeed(import_chalk3.default.green(`Created dev proxy "${name}".`));
816
+ } catch (err) {
817
+ spinner.fail("Failed to create the dev proxy.");
818
+ throw err;
819
+ }
820
+ const version2 = result.api_version || "1.0.0";
821
+ const endpoint = `https://${name}.apiblaze.com/${version2}/dev`;
822
+ console.log(` ${import_chalk3.default.dim("Endpoint:")} ${import_chalk3.default.bold(endpoint)}`);
823
+ if (auth === "api_key") {
824
+ const key = result.api_keys?.dev ?? Object.values(result.api_keys ?? {})[0];
825
+ if (key) {
826
+ console.log(` ${import_chalk3.default.dim("API key (dev):")} ${import_chalk3.default.bold.green(key)}`);
827
+ console.log(import_chalk3.default.dim(" Send it as the X-API-Key header. It may not be shown again."));
828
+ }
829
+ }
830
+ const targets = await getLocalhostTargets(teamId).catch(() => []);
831
+ const created = targets.find((t) => t.projectId === result.project_id);
832
+ if (!created) {
833
+ console.log(import_chalk3.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
834
+ return null;
835
+ }
836
+ return created;
837
+ }
838
+ async function probeLocalServer(port) {
839
+ const controller = new AbortController();
840
+ const timer = setTimeout(() => controller.abort(), 1500);
841
+ try {
842
+ await fetch(`http://127.0.0.1:${port}/`, { method: "HEAD", signal: controller.signal });
843
+ return true;
844
+ } catch (err) {
845
+ if (err?.name === "AbortError") return true;
846
+ const code = err?.cause?.code;
847
+ return !(code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH");
848
+ } finally {
849
+ clearTimeout(timer);
850
+ }
851
+ }
518
852
  async function runDev(options) {
519
853
  const creds = loadCredentials();
520
854
  if (!creds) {
@@ -555,13 +889,15 @@ async function runDev(options) {
555
889
  throw err;
556
890
  }
557
891
  }
558
- if (targets.length === 0) {
559
- console.log(import_chalk3.default.yellow("No projects found with an internal target."));
560
- console.log("Set a project's upstream target to localhost or a private IP in your APIblaze dashboard, then try again.");
561
- process.exit(0);
562
- }
563
892
  let selectedTargets;
564
- if (targets.length === 1) {
893
+ if (targets.length === 0) {
894
+ const created = await offerAutoCreate(teamId, options.port);
895
+ if (!created) {
896
+ console.log("Set a project's upstream target to localhost or a private IP, then try again.");
897
+ process.exit(0);
898
+ }
899
+ selectedTargets = [created];
900
+ } else if (targets.length === 1) {
565
901
  const { confirmed } = await import_inquirer.default.prompt([{
566
902
  type: "confirm",
567
903
  name: "confirmed",
@@ -595,6 +931,14 @@ async function runDev(options) {
595
931
  Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
596
932
  `)
597
933
  );
934
+ let recordSink;
935
+ let captureStream;
936
+ if (options.captureFile) {
937
+ captureStream = import_fs.default.createWriteStream(options.captureFile, { flags: "a" });
938
+ recordSink = (r) => captureStream.write(JSON.stringify(r) + "\n");
939
+ console.log(import_chalk3.default.gray(`Streaming full traffic to ${options.captureFile}
940
+ `));
941
+ }
598
942
  let restore = [];
599
943
  let connect;
600
944
  {
@@ -618,11 +962,27 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
618
962
  projectId,
619
963
  localPort: options.port,
620
964
  onEntry: (entry) => console.log(formatLogLine(entry)),
621
- onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`))
965
+ onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`)),
966
+ onCapture: (req, note) => console.log(formatCapturedRequest(req, note)),
967
+ onCaptureStart: () => console.log(
968
+ import_chalk3.default.magenta(`
969
+ \u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
970
+ `)
971
+ ),
972
+ onResume: () => console.log(
973
+ import_chalk3.default.green(`
974
+ \u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
975
+ `)
976
+ ),
977
+ onRecord: recordSink
622
978
  })
623
979
  );
980
+ const localUp = await probeLocalServer(options.port);
624
981
  console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
625
982
  console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
983
+ console.log(
984
+ localUp ? import_chalk3.default.green(`\u2713 Local server detected on port ${options.port} \u2014 forwarding live.`) : import_chalk3.default.magenta(`\u26B2 Nothing listening on port ${options.port} yet \u2014 requests will be captured until your server starts.`)
985
+ );
626
986
  console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
627
987
  let isCleaningUp = false;
628
988
  async function cleanup() {
@@ -630,6 +990,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
630
990
  isCleaningUp = true;
631
991
  console.log(import_chalk3.default.gray("\n\nShutting down..."));
632
992
  for (const client of clients) client.close();
993
+ captureStream?.end();
633
994
  await deleteDevTunnel(restore).catch(() => {
634
995
  });
635
996
  console.log(import_chalk3.default.green("Tunnel stopped."));
@@ -702,7 +1063,7 @@ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
702
1063
  }
703
1064
 
704
1065
  // src/commands/create.ts
705
- var import_fs = __toESM(require("fs"));
1066
+ var import_fs2 = __toESM(require("fs"));
706
1067
  var import_chalk5 = __toESM(require("chalk"));
707
1068
  var import_ora4 = __toESM(require("ora"));
708
1069
  init_auth();
@@ -937,7 +1298,7 @@ async function runAnonymousCreate(opts) {
937
1298
  if (opts.config) {
938
1299
  let raw = "";
939
1300
  try {
940
- raw = import_fs.default.readFileSync(opts.config, "utf8");
1301
+ raw = import_fs2.default.readFileSync(opts.config, "utf8");
941
1302
  } catch {
942
1303
  fail(`Cannot read --config file: ${opts.config}`);
943
1304
  }
@@ -1582,14 +1943,14 @@ program.command("mcp").description("Design an MCP server from the spec + traffic
1582
1943
  process.exit(1);
1583
1944
  }
1584
1945
  });
1585
- program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").action(async (port, opts) => {
1946
+ program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").option("-o, --capture-file <path>", "Stream full request/response traffic to a file (JSON lines)").action(async (port, opts) => {
1586
1947
  try {
1587
1948
  const resolved = parseInt(port ?? opts.port, 10);
1588
1949
  if (Number.isNaN(resolved)) {
1589
1950
  console.error(import_chalk14.default.red(`Invalid port: ${port ?? opts.port}`));
1590
1951
  process.exit(1);
1591
1952
  }
1592
- await runDev({ port: resolved });
1953
+ await runDev({ port: resolved, captureFile: opts.captureFile });
1593
1954
  } catch (err) {
1594
1955
  printError(err);
1595
1956
  process.exit(1);
@@ -1609,11 +1970,15 @@ Examples:
1609
1970
  $ npx apiblaze login
1610
1971
  $ npx apiblaze create --name myapi --target https://api.example.com --auth api_key
1611
1972
 
1973
+ # Dev tunnel \u2014 auto-creates a proxy if none point here, and captures
1974
+ # traffic (full headers + body, secrets masked) until your server is up:
1975
+ $ npx apiblaze dev 3000
1976
+ $ npx apiblaze dev 3000 --capture-file traffic.jsonl
1977
+
1612
1978
  # Manage:
1613
1979
  $ npx apiblaze whoami
1614
1980
  $ npx apiblaze projects
1615
1981
  $ npx apiblaze team
1616
- $ npx apiblaze dev 3000
1617
1982
  $ npx apiblaze logout
1618
1983
  `
1619
1984
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Dev tunnel CLI for APIblaze — route localhost projects through your APIblaze endpoints",
5
5
  "keywords": [
6
6
  "apiblaze",