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.
- package/README.md +14 -0
- package/dist/index.js +389 -24
- 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.
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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 =
|
|
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
|
);
|