apiblaze 0.1.10 → 0.1.13

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 +184 -205
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -69,9 +69,6 @@ function getAccessToken() {
69
69
  }
70
70
  return creds.accessToken;
71
71
  }
72
- function getApiblazeDir() {
73
- return APIBLAZE_DIR;
74
- }
75
72
  var fs, os, path, APIBLAZE_DIR, CREDENTIALS_PATH;
76
73
  var init_auth = __esm({
77
74
  "src/lib/auth.ts"() {
@@ -95,9 +92,9 @@ __export(api_exports, {
95
92
  getTeams: () => getTeams,
96
93
  putDevTunnel: () => putDevTunnel
97
94
  });
98
- async function apiFetch(path3, options = {}) {
95
+ async function apiFetch(path2, options = {}) {
99
96
  const token = getAccessToken();
100
- const url = `${DASHBOARD_BASE}${path3}`;
97
+ const url = `${DASHBOARD_BASE}${path2}`;
101
98
  const res = await fetch(url, {
102
99
  ...options,
103
100
  headers: {
@@ -150,8 +147,11 @@ async function putDevTunnel(payload) {
150
147
  body: JSON.stringify(payload)
151
148
  });
152
149
  }
153
- async function deleteDevTunnel() {
154
- return apiFetch("/api/cli/dev-tunnel", { method: "DELETE" });
150
+ async function deleteDevTunnel(restore) {
151
+ return apiFetch("/api/cli/dev-tunnel", {
152
+ method: "DELETE",
153
+ body: JSON.stringify({ restore })
154
+ });
155
155
  }
156
156
  var DASHBOARD_BASE;
157
157
  var init_api = __esm({
@@ -285,129 +285,13 @@ ${import_chalk.default.cyan("\u2192")} Team: ${import_chalk.default.bold(teamNam
285
285
 
286
286
  // src/commands/dev.ts
287
287
  var import_chalk3 = __toESM(require("chalk"));
288
- var import_ora3 = __toESM(require("ora"));
288
+ var import_ora2 = __toESM(require("ora"));
289
289
  var import_inquirer = __toESM(require("inquirer"));
290
290
  init_auth();
291
291
  init_api();
292
292
 
293
- // src/lib/cloudflared.ts
294
- var fs2 = __toESM(require("fs"));
295
- var path2 = __toESM(require("path"));
296
- var readline = __toESM(require("readline"));
297
- var import_child_process = require("child_process");
298
- var import_ora2 = __toESM(require("ora"));
299
- init_auth();
300
- var TUNNEL_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
301
- var DOWNLOAD_TIMEOUT_MS = 3e4;
302
- var TUNNEL_START_TIMEOUT_MS = 3e4;
303
- function getCloudflaredFilename() {
304
- const platform = process.platform;
305
- const arch = process.arch;
306
- if (platform === "darwin" && arch === "arm64") return "cloudflared-darwin-arm64";
307
- if (platform === "darwin" && arch === "x64") return "cloudflared-darwin-amd64";
308
- if (platform === "linux" && arch === "arm64") return "cloudflared-linux-arm64";
309
- if (platform === "linux" && arch === "x64") return "cloudflared-linux-amd64";
310
- if (platform === "win32" && arch === "x64") return "cloudflared-windows-amd64.exe";
311
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
312
- }
313
- function getCloudflaredBinPath() {
314
- const filename = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
315
- return path2.join(getApiblazeDir(), "bin", filename);
316
- }
317
- function isExecutable(filePath) {
318
- try {
319
- fs2.accessSync(filePath, fs2.constants.X_OK);
320
- return true;
321
- } catch {
322
- return false;
323
- }
324
- }
325
- async function downloadCloudflared(binPath) {
326
- const filename = getCloudflaredFilename();
327
- const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${filename}`;
328
- const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
329
- if (!res.ok || !res.body) {
330
- throw new Error(`Failed to download cloudflared: ${res.status} ${res.statusText}`);
331
- }
332
- fs2.mkdirSync(path2.dirname(binPath), { recursive: true });
333
- const tmpPath = `${binPath}.tmp`;
334
- const writer = fs2.createWriteStream(tmpPath);
335
- const reader = res.body.getReader();
336
- try {
337
- while (true) {
338
- const { done, value } = await reader.read();
339
- if (done) break;
340
- await new Promise((resolve, reject) => {
341
- writer.write(value, (err) => err ? reject(err) : resolve());
342
- });
343
- }
344
- } finally {
345
- reader.releaseLock();
346
- }
347
- await new Promise((resolve, reject) => {
348
- writer.end((err) => err ? reject(err) : resolve());
349
- });
350
- fs2.renameSync(tmpPath, binPath);
351
- if (process.platform !== "win32") {
352
- fs2.chmodSync(binPath, 493);
353
- }
354
- }
355
- async function ensureCloudflared() {
356
- const binPath = getCloudflaredBinPath();
357
- if (fs2.existsSync(binPath) && isExecutable(binPath)) {
358
- return;
359
- }
360
- const spinner = (0, import_ora2.default)("Downloading cloudflared...").start();
361
- try {
362
- await downloadCloudflared(binPath);
363
- spinner.succeed("cloudflared downloaded.");
364
- } catch (err) {
365
- spinner.fail("Failed to download cloudflared.");
366
- throw err;
367
- }
368
- }
369
- function spawnCloudflared(port) {
370
- return new Promise((resolve, reject) => {
371
- const binPath = getCloudflaredBinPath();
372
- const proc = (0, import_child_process.spawn)(binPath, ["tunnel", "--url", `http://localhost:${port}`], {
373
- stdio: ["ignore", "ignore", "pipe"]
374
- });
375
- const timer = setTimeout(() => {
376
- proc.kill();
377
- reject(new Error("Timed out waiting for cloudflared tunnel URL (30s). Is something already running on the port?"));
378
- }, TUNNEL_START_TIMEOUT_MS);
379
- const rl = readline.createInterface({ input: proc.stderr });
380
- rl.on("line", (line) => {
381
- const match = line.match(TUNNEL_URL_RE);
382
- if (match) {
383
- clearTimeout(timer);
384
- rl.close();
385
- resolve({ process: proc, tunnelUrl: match[0] });
386
- }
387
- });
388
- proc.on("error", (err) => {
389
- clearTimeout(timer);
390
- reject(new Error(`Failed to start cloudflared: ${err.message}`));
391
- });
392
- proc.on("exit", (code) => {
393
- clearTimeout(timer);
394
- if (code !== null && code !== 0) {
395
- reject(new Error(`cloudflared exited with code ${code}`));
396
- }
397
- });
398
- });
399
- }
400
- function killCloudflared(proc) {
401
- try {
402
- proc.kill("SIGTERM");
403
- } catch {
404
- }
405
- }
406
-
407
293
  // src/lib/traffic.ts
408
- var import_ws = __toESM(require("ws"));
409
294
  var import_chalk2 = __toESM(require("chalk"));
410
- init_auth();
411
295
  var METHOD_COLORS = {
412
296
  GET: import_chalk2.default.cyan,
413
297
  POST: import_chalk2.default.green,
@@ -439,35 +323,136 @@ function formatLogLine(entry) {
439
323
  const ts = now.toTimeString().slice(0, 8);
440
324
  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)})`)}`;
441
325
  }
442
- function connectTrafficStream(wsUrl, onEntry) {
443
- const token = getAccessToken();
444
- let ws;
445
- let reconnectCount = 0;
446
- const MAX_RECONNECTS = 3;
326
+
327
+ // src/lib/tunnel-client.ts
328
+ var import_ws = __toESM(require("ws"));
329
+ var CHUNK_BYTES = 512 * 1024;
330
+ var PING_INTERVAL_MS = 6e4;
331
+ var MAX_RECONNECT_DELAY_MS = 15e3;
332
+ var STRIP_HEADERS = /* @__PURE__ */ new Set([
333
+ "host",
334
+ "content-length",
335
+ "connection",
336
+ "keep-alive",
337
+ "transfer-encoding",
338
+ "upgrade",
339
+ "proxy-connection",
340
+ "expect"
341
+ ]);
342
+ function stripHeaders(headers) {
343
+ const out = {};
344
+ for (const key of Object.keys(headers)) {
345
+ if (!STRIP_HEADERS.has(key.toLowerCase())) out[key] = headers[key];
346
+ }
347
+ return out;
348
+ }
349
+ function startTunnelClient(opts) {
350
+ const target = `http://127.0.0.1:${opts.localPort}`;
351
+ const inflight = /* @__PURE__ */ new Map();
352
+ let ws = null;
353
+ let closed = false;
354
+ let reconnects = 0;
355
+ let pingTimer;
447
356
  function connect() {
448
- ws = new import_ws.default(wsUrl, { headers: { Authorization: `Bearer ${token}` } });
449
- ws.on("message", (data) => {
450
- try {
451
- const entry = JSON.parse(data.toString());
452
- onEntry(entry);
453
- } catch {
454
- }
357
+ const url = `${opts.connectUrl}?project=${encodeURIComponent(opts.projectId)}&token=${encodeURIComponent(opts.token)}`;
358
+ const socket = new import_ws.default(url);
359
+ ws = socket;
360
+ socket.on("open", () => {
361
+ reconnects = 0;
362
+ opts.onStatus?.("connected");
363
+ pingTimer = setInterval(() => {
364
+ try {
365
+ socket.send("ping");
366
+ } catch {
367
+ }
368
+ }, PING_INTERVAL_MS);
455
369
  });
456
- ws.on("close", () => {
457
- if (reconnectCount < MAX_RECONNECTS) {
458
- reconnectCount++;
459
- setTimeout(() => connect(), 2e3);
460
- } else {
461
- console.warn(import_chalk2.default.yellow("\nTraffic stream disconnected. Stop and restart `apiblaze dev` to reconnect."));
462
- }
370
+ socket.on("message", (data) => handleFrame(socket, data.toString()));
371
+ socket.on("close", () => {
372
+ if (pingTimer) clearInterval(pingTimer);
373
+ if (!closed) scheduleReconnect();
463
374
  });
464
- ws.on("error", (err) => {
465
- console.warn(import_chalk2.default.yellow(`
466
- Traffic stream error: ${err.message}`));
375
+ socket.on("error", () => {
467
376
  });
468
- return ws;
469
377
  }
470
- return connect();
378
+ function scheduleReconnect() {
379
+ const delay = Math.min(1e3 * 2 ** reconnects, MAX_RECONNECT_DELAY_MS);
380
+ reconnects++;
381
+ opts.onStatus?.(`disconnected \u2014 reconnecting in ${Math.round(delay / 1e3)}s`);
382
+ setTimeout(() => {
383
+ if (!closed) connect();
384
+ }, delay);
385
+ }
386
+ function handleFrame(socket, raw) {
387
+ let frame;
388
+ try {
389
+ frame = JSON.parse(raw);
390
+ } catch {
391
+ return;
392
+ }
393
+ if (frame.type === "req") {
394
+ inflight.set(frame.id, {
395
+ method: frame.method,
396
+ path: frame.path,
397
+ headers: frame.headers ?? {},
398
+ chunks: [],
399
+ start: Date.now()
400
+ });
401
+ if (frame.bodyLen === 0) finish(socket, frame.id);
402
+ } else if (frame.type === "chunk") {
403
+ const f = inflight.get(frame.id);
404
+ if (!f) return;
405
+ f.chunks.push(Buffer.from(frame.data ?? "", "base64"));
406
+ if (frame.final) finish(socket, frame.id);
407
+ }
408
+ }
409
+ function finish(socket, id) {
410
+ const f = inflight.get(id);
411
+ if (!f) return;
412
+ inflight.delete(id);
413
+ void forward(socket, id, f);
414
+ }
415
+ async function forward(socket, id, f) {
416
+ const body = Buffer.concat(f.chunks);
417
+ const init = { method: f.method, headers: stripHeaders(f.headers) };
418
+ if (body.length) init.body = body;
419
+ let status = 502;
420
+ try {
421
+ const resp = await fetch(target + f.path, init);
422
+ status = resp.status;
423
+ const buf = Buffer.from(await resp.arrayBuffer());
424
+ const headers = {};
425
+ resp.headers.forEach((value, key) => {
426
+ if (!STRIP_HEADERS.has(key.toLowerCase())) headers[key] = value;
427
+ });
428
+ send(socket, { id, type: "res", status, headers, bodyLen: buf.length });
429
+ for (let off = 0, seq = 0; off < buf.length; off += CHUNK_BYTES) {
430
+ const slice = buf.subarray(off, Math.min(off + CHUNK_BYTES, buf.length));
431
+ send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= buf.length });
432
+ }
433
+ } catch (err) {
434
+ const message = err?.cause?.message || err?.message || String(err);
435
+ send(socket, { id, type: "err", message });
436
+ }
437
+ opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
438
+ }
439
+ function send(socket, frame) {
440
+ try {
441
+ socket.send(JSON.stringify(frame));
442
+ } catch {
443
+ }
444
+ }
445
+ connect();
446
+ return {
447
+ close() {
448
+ closed = true;
449
+ if (pingTimer) clearInterval(pingTimer);
450
+ try {
451
+ ws?.close();
452
+ } catch {
453
+ }
454
+ }
455
+ };
471
456
  }
472
457
 
473
458
  // src/commands/dev.ts
@@ -502,7 +487,7 @@ async function runDev(options) {
502
487
  }
503
488
  let targets;
504
489
  {
505
- const spinner = (0, import_ora3.default)("Fetching your localhost projects...").start();
490
+ const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
506
491
  try {
507
492
  targets = await getLocalhostTargets(teamId);
508
493
  spinner.stop();
@@ -512,8 +497,8 @@ async function runDev(options) {
512
497
  }
513
498
  }
514
499
  if (targets.length === 0) {
515
- console.log(import_chalk3.default.yellow("No projects found with localhost targets."));
516
- console.log("Set a project's upstream target to localhost in your APIblaze dashboard, then try again.");
500
+ console.log(import_chalk3.default.yellow("No projects found with an internal target."));
501
+ console.log("Set a project's upstream target to localhost or a private IP in your APIblaze dashboard, then try again.");
517
502
  process.exit(0);
518
503
  }
519
504
  let selectedTargets;
@@ -521,7 +506,7 @@ async function runDev(options) {
521
506
  const { confirmed } = await import_inquirer.default.prompt([{
522
507
  type: "confirm",
523
508
  name: "confirmed",
524
- message: `Found 1 project targeting localhost \u2014 tunnel "${import_chalk3.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
509
+ message: `Found 1 project with an internal target \u2014 tunnel "${import_chalk3.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
525
510
  default: true
526
511
  }]);
527
512
  if (!confirmed) {
@@ -533,7 +518,7 @@ async function runDev(options) {
533
518
  const { chosen } = await import_inquirer.default.prompt([{
534
519
  type: "checkbox",
535
520
  name: "chosen",
536
- message: `Found ${targets.length} projects targeting localhost \u2014 select which to tunnel:`,
521
+ message: `Found ${targets.length} projects with an internal target \u2014 select which to tunnel:`,
537
522
  choices: targets.map((t) => ({
538
523
  name: `${import_chalk3.default.bold(t.projectName)} (${t.tenantName}) \u2014 ${t.target}`,
539
524
  value: t,
@@ -551,66 +536,55 @@ async function runDev(options) {
551
536
  Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
552
537
  `)
553
538
  );
554
- await ensureCloudflared();
555
- let cfProcess;
556
- let tunnelUrl;
557
- {
558
- const spinner = (0, import_ora3.default)("Starting Cloudflare tunnel...").start();
559
- try {
560
- ({ process: cfProcess, tunnelUrl } = await spawnCloudflared(options.port));
561
- spinner.succeed(`Tunnel active: ${import_chalk3.default.bold.cyan(tunnelUrl)}`);
562
- } catch (err) {
563
- spinner.fail("Failed to start cloudflared tunnel.");
564
- throw err;
565
- }
566
- }
539
+ let restore = [];
540
+ let connect;
567
541
  {
568
- const spinner = (0, import_ora3.default)("Registering tunnel with APIblaze...").start();
569
- let wsUrl;
542
+ const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
570
543
  try {
571
544
  const result = await putDevTunnel({
572
- tunnelUrl,
573
545
  targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
574
546
  });
575
- wsUrl = result.wsUrl;
576
- spinner.succeed("Tunnel registered. Proxying traffic.");
547
+ restore = result.restore ?? [];
548
+ connect = result.connect;
549
+ spinner.succeed("Tunnel registered.");
577
550
  } catch (err) {
578
551
  spinner.fail("Failed to register tunnel.");
579
- killCloudflared(cfProcess);
580
552
  throw err;
581
553
  }
582
- let ws;
583
- console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
584
- console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
585
- console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
586
- ws = connectTrafficStream(wsUrl, (entry) => {
587
- console.log(formatLogLine(entry));
588
- });
589
- let isCleaningUp = false;
590
- async function cleanup() {
591
- if (isCleaningUp) return;
592
- isCleaningUp = true;
593
- console.log(import_chalk3.default.gray("\n\nShutting down..."));
594
- try {
595
- ws.close();
596
- } catch {
597
- }
598
- await deleteDevTunnel().catch(() => {
599
- });
600
- killCloudflared(cfProcess);
601
- console.log(import_chalk3.default.green("Tunnel stopped."));
602
- process.exit(0);
603
- }
604
- process.on("SIGINT", () => void cleanup());
605
- process.on("SIGTERM", () => void cleanup());
606
- await new Promise(() => {
554
+ }
555
+ const clients = connect.projects.map(
556
+ (projectId) => startTunnelClient({
557
+ connectUrl: connect.url,
558
+ token: connect.token,
559
+ projectId,
560
+ localPort: options.port,
561
+ onEntry: (entry) => console.log(formatLogLine(entry)),
562
+ onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`))
563
+ })
564
+ );
565
+ console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
566
+ console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
567
+ console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
568
+ let isCleaningUp = false;
569
+ async function cleanup() {
570
+ if (isCleaningUp) return;
571
+ isCleaningUp = true;
572
+ console.log(import_chalk3.default.gray("\n\nShutting down..."));
573
+ for (const client of clients) client.close();
574
+ await deleteDevTunnel(restore).catch(() => {
607
575
  });
576
+ console.log(import_chalk3.default.green("Tunnel stopped."));
577
+ process.exit(0);
608
578
  }
579
+ process.on("SIGINT", () => void cleanup());
580
+ process.on("SIGTERM", () => void cleanup());
581
+ await new Promise(() => {
582
+ });
609
583
  }
610
584
 
611
585
  // src/commands/projects.ts
612
586
  var import_chalk4 = __toESM(require("chalk"));
613
- var import_ora4 = __toESM(require("ora"));
587
+ var import_ora3 = __toESM(require("ora"));
614
588
  init_auth();
615
589
  init_api();
616
590
  async function runProjects() {
@@ -647,7 +621,7 @@ async function runProjects() {
647
621
  }
648
622
  console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(teamName ?? teamId)}
649
623
  `);
650
- const spinner = (0, import_ora4.default)("Fetching projects...").start();
624
+ const spinner = (0, import_ora3.default)("Fetching projects...").start();
651
625
  let projects;
652
626
  try {
653
627
  projects = await getProjects(teamId);
@@ -670,7 +644,7 @@ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
670
644
 
671
645
  // src/commands/create.ts
672
646
  var import_chalk5 = __toESM(require("chalk"));
673
- var import_ora5 = __toESM(require("ora"));
647
+ var import_ora4 = __toESM(require("ora"));
674
648
  init_auth();
675
649
  init_api();
676
650
  function normalizeName(raw) {
@@ -749,7 +723,7 @@ async function runCreate(opts = {}) {
749
723
  console.log(import_chalk5.default.yellow(" Name must be at least 3 characters (letters and digits only).\n"));
750
724
  continue;
751
725
  }
752
- const spinner2 = (0, import_ora5.default)("Checking availability...").start();
726
+ const spinner2 = (0, import_ora4.default)("Checking availability...").start();
753
727
  try {
754
728
  const check = await checkProxyName(name, teamId);
755
729
  spinner2.stop();
@@ -805,7 +779,7 @@ async function runCreate(opts = {}) {
805
779
  return;
806
780
  }
807
781
  }
808
- const spinner = !opts.json ? (0, import_ora5.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
782
+ const spinner = !opts.json ? (0, import_ora4.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
809
783
  let result;
810
784
  try {
811
785
  result = await createProxy({ name, target_url: targetUrl, auth_type: auth, team_id: teamId });
@@ -895,7 +869,7 @@ async function runTeam(arg) {
895
869
 
896
870
  // src/index.ts
897
871
  var program = new import_commander.Command();
898
- program.name("apiblaze").description("APIblaze dev tunnel CLI").version("0.1.0");
872
+ program.name("apiblaze").description("APIblaze dev tunnel CLI").version("0.1.11");
899
873
  program.command("login").description("Authenticate with APIblaze").action(async () => {
900
874
  try {
901
875
  await runLogin();
@@ -928,9 +902,14 @@ program.command("projects").description("List the projects in your team").action
928
902
  process.exit(1);
929
903
  }
930
904
  });
931
- program.command("dev").description("Start a dev tunnel for your localhost projects").option("-p, --port <number>", "Local port to tunnel", "3000").action(async (opts) => {
905
+ 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) => {
932
906
  try {
933
- await runDev({ port: parseInt(opts.port, 10) });
907
+ const resolved = parseInt(port ?? opts.port, 10);
908
+ if (Number.isNaN(resolved)) {
909
+ console.error(import_chalk7.default.red(`Invalid port: ${port ?? opts.port}`));
910
+ process.exit(1);
911
+ }
912
+ await runDev({ port: resolved });
934
913
  } catch (err) {
935
914
  printError(err);
936
915
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.1.10",
4
- "description": "Dev tunnel CLI for APIblaze — route localhost projects through Cloudflare tunnels",
3
+ "version": "0.1.13",
4
+ "description": "Dev tunnel CLI for APIblaze — route localhost projects through your APIblaze endpoints",
5
5
  "keywords": [
6
6
  "apiblaze",
7
7
  "dev-tunnel",