apiblaze 0.1.12 → 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 +153 -194
  2. package/package.json +5 -3
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: {
@@ -288,125 +285,11 @@ ${import_chalk.default.cyan("\u2192")} Team: ${import_chalk.default.bold(teamNam
288
285
 
289
286
  // src/commands/dev.ts
290
287
  var import_chalk3 = __toESM(require("chalk"));
291
- var import_ora3 = __toESM(require("ora"));
288
+ var import_ora2 = __toESM(require("ora"));
292
289
  var import_inquirer = __toESM(require("inquirer"));
293
290
  init_auth();
294
291
  init_api();
295
292
 
296
- // src/lib/cloudflared.ts
297
- var fs2 = __toESM(require("fs"));
298
- var path2 = __toESM(require("path"));
299
- var readline = __toESM(require("readline"));
300
- var import_child_process = require("child_process");
301
- var import_ora2 = __toESM(require("ora"));
302
- init_auth();
303
- var TUNNEL_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
304
- var DOWNLOAD_TIMEOUT_MS = 3e4;
305
- var TUNNEL_START_TIMEOUT_MS = 3e4;
306
- function getCloudflaredFilename() {
307
- const platform = process.platform;
308
- const arch = process.arch;
309
- if (platform === "darwin" && arch === "arm64") return "cloudflared-darwin-arm64";
310
- if (platform === "darwin" && arch === "x64") return "cloudflared-darwin-amd64";
311
- if (platform === "linux" && arch === "arm64") return "cloudflared-linux-arm64";
312
- if (platform === "linux" && arch === "x64") return "cloudflared-linux-amd64";
313
- if (platform === "win32" && arch === "x64") return "cloudflared-windows-amd64.exe";
314
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
315
- }
316
- function getCloudflaredBinPath() {
317
- const filename = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
318
- return path2.join(getApiblazeDir(), "bin", filename);
319
- }
320
- function isExecutable(filePath) {
321
- try {
322
- fs2.accessSync(filePath, fs2.constants.X_OK);
323
- return true;
324
- } catch {
325
- return false;
326
- }
327
- }
328
- async function downloadCloudflared(binPath) {
329
- const filename = getCloudflaredFilename();
330
- const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${filename}`;
331
- const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
332
- if (!res.ok || !res.body) {
333
- throw new Error(`Failed to download cloudflared: ${res.status} ${res.statusText}`);
334
- }
335
- fs2.mkdirSync(path2.dirname(binPath), { recursive: true });
336
- const tmpPath = `${binPath}.tmp`;
337
- const writer = fs2.createWriteStream(tmpPath);
338
- const reader = res.body.getReader();
339
- try {
340
- while (true) {
341
- const { done, value } = await reader.read();
342
- if (done) break;
343
- await new Promise((resolve, reject) => {
344
- writer.write(value, (err) => err ? reject(err) : resolve());
345
- });
346
- }
347
- } finally {
348
- reader.releaseLock();
349
- }
350
- await new Promise((resolve, reject) => {
351
- writer.end((err) => err ? reject(err) : resolve());
352
- });
353
- fs2.renameSync(tmpPath, binPath);
354
- if (process.platform !== "win32") {
355
- fs2.chmodSync(binPath, 493);
356
- }
357
- }
358
- async function ensureCloudflared() {
359
- const binPath = getCloudflaredBinPath();
360
- if (fs2.existsSync(binPath) && isExecutable(binPath)) {
361
- return;
362
- }
363
- const spinner = (0, import_ora2.default)("Downloading cloudflared...").start();
364
- try {
365
- await downloadCloudflared(binPath);
366
- spinner.succeed("cloudflared downloaded.");
367
- } catch (err) {
368
- spinner.fail("Failed to download cloudflared.");
369
- throw err;
370
- }
371
- }
372
- function spawnCloudflared(port) {
373
- return new Promise((resolve, reject) => {
374
- const binPath = getCloudflaredBinPath();
375
- const proc = (0, import_child_process.spawn)(binPath, ["tunnel", "--url", `http://localhost:${port}`], {
376
- stdio: ["ignore", "ignore", "pipe"]
377
- });
378
- const timer = setTimeout(() => {
379
- proc.kill();
380
- reject(new Error("Timed out waiting for cloudflared tunnel URL (30s). Is something already running on the port?"));
381
- }, TUNNEL_START_TIMEOUT_MS);
382
- const rl = readline.createInterface({ input: proc.stderr });
383
- rl.on("line", (line) => {
384
- const match = line.match(TUNNEL_URL_RE);
385
- if (match) {
386
- clearTimeout(timer);
387
- rl.close();
388
- resolve({ process: proc, tunnelUrl: match[0] });
389
- }
390
- });
391
- proc.on("error", (err) => {
392
- clearTimeout(timer);
393
- reject(new Error(`Failed to start cloudflared: ${err.message}`));
394
- });
395
- proc.on("exit", (code) => {
396
- clearTimeout(timer);
397
- if (code !== null && code !== 0) {
398
- reject(new Error(`cloudflared exited with code ${code}`));
399
- }
400
- });
401
- });
402
- }
403
- function killCloudflared(proc) {
404
- try {
405
- proc.kill("SIGTERM");
406
- } catch {
407
- }
408
- }
409
-
410
293
  // src/lib/traffic.ts
411
294
  var import_chalk2 = __toESM(require("chalk"));
412
295
  var METHOD_COLORS = {
@@ -441,50 +324,135 @@ function formatLogLine(entry) {
441
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)})`)}`;
442
325
  }
443
326
 
444
- // src/lib/proxy-logger.ts
445
- var import_http = __toESM(require("http"));
446
- var import_net = __toESM(require("net"));
447
- function startPassthroughProxy(targetPort, onEntry) {
448
- return new Promise((resolve, reject) => {
449
- const server = import_http.default.createServer((req, res) => {
450
- const start = Date.now();
451
- const method = req.method ?? "GET";
452
- const path3 = req.url ?? "/";
453
- const proxyReq = import_http.default.request(
454
- { host: "127.0.0.1", port: targetPort, method, path: path3, headers: req.headers },
455
- (proxyRes) => {
456
- onEntry({ method, path: path3, status: proxyRes.statusCode ?? 0, latency: Date.now() - start });
457
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
458
- proxyRes.pipe(res);
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;
356
+ function connect() {
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 {
459
367
  }
460
- );
461
- proxyReq.on("error", () => {
462
- onEntry({ method, path: path3, status: 502, latency: Date.now() - start });
463
- if (!res.headersSent) res.writeHead(502);
464
- res.end();
465
- });
466
- req.pipe(proxyReq);
368
+ }, PING_INTERVAL_MS);
467
369
  });
468
- server.on("upgrade", (req, clientSocket, head) => {
469
- const upstream = import_net.default.connect(targetPort, "127.0.0.1", () => {
470
- const headerLines = Object.entries(req.headers).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : v}`).join("\r\n");
471
- upstream.write(`${req.method} ${req.url} HTTP/1.1\r
472
- ${headerLines}\r
473
- \r
474
- `);
475
- if (head && head.length) upstream.write(head);
476
- upstream.pipe(clientSocket);
477
- clientSocket.pipe(upstream);
478
- });
479
- upstream.on("error", () => clientSocket.destroy());
480
- clientSocket.on("error", () => upstream.destroy());
370
+ socket.on("message", (data) => handleFrame(socket, data.toString()));
371
+ socket.on("close", () => {
372
+ if (pingTimer) clearInterval(pingTimer);
373
+ if (!closed) scheduleReconnect();
481
374
  });
482
- server.on("error", reject);
483
- server.listen(0, "127.0.0.1", () => {
484
- const { port } = server.address();
485
- resolve({ port, close: () => server.close() });
375
+ socket.on("error", () => {
486
376
  });
487
- });
377
+ }
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
+ };
488
456
  }
489
457
 
490
458
  // src/commands/dev.ts
@@ -519,7 +487,7 @@ async function runDev(options) {
519
487
  }
520
488
  let targets;
521
489
  {
522
- const spinner = (0, import_ora3.default)("Fetching your localhost projects...").start();
490
+ const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
523
491
  try {
524
492
  targets = await getLocalhostTargets(teamId);
525
493
  spinner.stop();
@@ -529,8 +497,8 @@ async function runDev(options) {
529
497
  }
530
498
  }
531
499
  if (targets.length === 0) {
532
- console.log(import_chalk3.default.yellow("No projects found with localhost targets."));
533
- 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.");
534
502
  process.exit(0);
535
503
  }
536
504
  let selectedTargets;
@@ -538,7 +506,7 @@ async function runDev(options) {
538
506
  const { confirmed } = await import_inquirer.default.prompt([{
539
507
  type: "confirm",
540
508
  name: "confirmed",
541
- 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})?`,
542
510
  default: true
543
511
  }]);
544
512
  if (!confirmed) {
@@ -550,7 +518,7 @@ async function runDev(options) {
550
518
  const { chosen } = await import_inquirer.default.prompt([{
551
519
  type: "checkbox",
552
520
  name: "chosen",
553
- 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:`,
554
522
  choices: targets.map((t) => ({
555
523
  name: `${import_chalk3.default.bold(t.projectName)} (${t.tenantName}) \u2014 ${t.target}`,
556
524
  value: t,
@@ -568,40 +536,32 @@ async function runDev(options) {
568
536
  Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
569
537
  `)
570
538
  );
571
- await ensureCloudflared();
572
- const logger = await startPassthroughProxy(options.port, (entry) => {
573
- console.log(formatLogLine(entry));
574
- });
575
- let cfProcess;
576
- let tunnelUrl;
577
- {
578
- const spinner = (0, import_ora3.default)("Starting Cloudflare tunnel...").start();
579
- try {
580
- ({ process: cfProcess, tunnelUrl } = await spawnCloudflared(logger.port));
581
- spinner.succeed(`Tunnel active: ${import_chalk3.default.bold.cyan(tunnelUrl)}`);
582
- } catch (err) {
583
- spinner.fail("Failed to start cloudflared tunnel.");
584
- logger.close();
585
- throw err;
586
- }
587
- }
588
539
  let restore = [];
540
+ let connect;
589
541
  {
590
- const spinner = (0, import_ora3.default)("Registering tunnel with APIblaze...").start();
542
+ const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
591
543
  try {
592
544
  const result = await putDevTunnel({
593
- tunnelUrl,
594
545
  targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
595
546
  });
596
547
  restore = result.restore ?? [];
597
- spinner.succeed("Tunnel registered. Proxying traffic.");
548
+ connect = result.connect;
549
+ spinner.succeed("Tunnel registered.");
598
550
  } catch (err) {
599
551
  spinner.fail("Failed to register tunnel.");
600
- logger.close();
601
- killCloudflared(cfProcess);
602
552
  throw err;
603
553
  }
604
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
+ );
605
565
  console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
606
566
  console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
607
567
  console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
@@ -610,10 +570,9 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
610
570
  if (isCleaningUp) return;
611
571
  isCleaningUp = true;
612
572
  console.log(import_chalk3.default.gray("\n\nShutting down..."));
573
+ for (const client of clients) client.close();
613
574
  await deleteDevTunnel(restore).catch(() => {
614
575
  });
615
- logger.close();
616
- killCloudflared(cfProcess);
617
576
  console.log(import_chalk3.default.green("Tunnel stopped."));
618
577
  process.exit(0);
619
578
  }
@@ -625,7 +584,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
625
584
 
626
585
  // src/commands/projects.ts
627
586
  var import_chalk4 = __toESM(require("chalk"));
628
- var import_ora4 = __toESM(require("ora"));
587
+ var import_ora3 = __toESM(require("ora"));
629
588
  init_auth();
630
589
  init_api();
631
590
  async function runProjects() {
@@ -662,7 +621,7 @@ async function runProjects() {
662
621
  }
663
622
  console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(teamName ?? teamId)}
664
623
  `);
665
- const spinner = (0, import_ora4.default)("Fetching projects...").start();
624
+ const spinner = (0, import_ora3.default)("Fetching projects...").start();
666
625
  let projects;
667
626
  try {
668
627
  projects = await getProjects(teamId);
@@ -685,7 +644,7 @@ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
685
644
 
686
645
  // src/commands/create.ts
687
646
  var import_chalk5 = __toESM(require("chalk"));
688
- var import_ora5 = __toESM(require("ora"));
647
+ var import_ora4 = __toESM(require("ora"));
689
648
  init_auth();
690
649
  init_api();
691
650
  function normalizeName(raw) {
@@ -764,7 +723,7 @@ async function runCreate(opts = {}) {
764
723
  console.log(import_chalk5.default.yellow(" Name must be at least 3 characters (letters and digits only).\n"));
765
724
  continue;
766
725
  }
767
- const spinner2 = (0, import_ora5.default)("Checking availability...").start();
726
+ const spinner2 = (0, import_ora4.default)("Checking availability...").start();
768
727
  try {
769
728
  const check = await checkProxyName(name, teamId);
770
729
  spinner2.stop();
@@ -820,7 +779,7 @@ async function runCreate(opts = {}) {
820
779
  return;
821
780
  }
822
781
  }
823
- 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;
824
783
  let result;
825
784
  try {
826
785
  result = await createProxy({ name, target_url: targetUrl, auth_type: auth, team_id: teamId });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.1.12",
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",
@@ -32,11 +32,13 @@
32
32
  "chalk": "^4.1.2",
33
33
  "commander": "^11.1.0",
34
34
  "inquirer": "^8.2.6",
35
- "ora": "^5.4.1"
35
+ "ora": "^5.4.1",
36
+ "ws": "^8.17.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/inquirer": "^8.2.10",
39
40
  "@types/node": "^20.0.0",
41
+ "@types/ws": "^8.5.10",
40
42
  "tsup": "^8.0.0",
41
43
  "typescript": "^5.4.0"
42
44
  }