nbound 1.0.5 → 1.1.0

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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  // @bun
3
3
  import { createRequire } from "node:module";
4
4
  var __create = Object.create;
@@ -5223,7 +5223,7 @@ var require_websocket = __commonJS((exports, module) => {
5223
5223
  var http = __require("http");
5224
5224
  var net = __require("net");
5225
5225
  var tls = __require("tls");
5226
- var { randomBytes: randomBytes2, createHash } = __require("crypto");
5226
+ var { randomBytes: randomBytes3, createHash } = __require("crypto");
5227
5227
  var { Duplex, Readable } = __require("stream");
5228
5228
  var { URL: URL2 } = __require("url");
5229
5229
  var PerMessageDeflate = require_permessage_deflate();
@@ -5632,7 +5632,7 @@ var require_websocket = __commonJS((exports, module) => {
5632
5632
  }
5633
5633
  }
5634
5634
  const defaultPort = isSecure ? 443 : 80;
5635
- const key = randomBytes2(16).toString("base64");
5635
+ const key = randomBytes3(16).toString("base64");
5636
5636
  const request2 = isSecure ? https.request : http.request;
5637
5637
  const protocolSet = new Set;
5638
5638
  let perMessageDeflate;
@@ -6441,7 +6441,8 @@ var {
6441
6441
  } = import__.default;
6442
6442
 
6443
6443
  // src/commands/login.ts
6444
- import { createInterface } from "node:readline";
6444
+ import { createServer } from "node:http";
6445
+ import { randomBytes as randomBytes2 } from "node:crypto";
6445
6446
 
6446
6447
  // ../../node_modules/.pnpm/open@10.2.0/node_modules/open/index.js
6447
6448
  import process7 from "node:process";
@@ -6940,36 +6941,6 @@ defineLazyProperty(apps, "browser", () => "browser");
6940
6941
  defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
6941
6942
  var open_default = open;
6942
6943
 
6943
- // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
6944
- import { webcrypto as crypto } from "node:crypto";
6945
-
6946
- // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/url-alphabet/index.js
6947
- var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
6948
-
6949
- // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
6950
- var POOL_SIZE_MULTIPLIER = 128;
6951
- var pool;
6952
- var poolOffset;
6953
- function fillPool(bytes) {
6954
- if (!pool || pool.length < bytes) {
6955
- pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
6956
- crypto.getRandomValues(pool);
6957
- poolOffset = 0;
6958
- } else if (poolOffset + bytes > pool.length) {
6959
- crypto.getRandomValues(pool);
6960
- poolOffset = 0;
6961
- }
6962
- poolOffset += bytes;
6963
- }
6964
- function nanoid(size = 21) {
6965
- fillPool(size |= 0);
6966
- let id = "";
6967
- for (let i = poolOffset - size;i < poolOffset; i++) {
6968
- id += urlAlphabet[pool[i] & 63];
6969
- }
6970
- return id;
6971
- }
6972
-
6973
6944
  // src/lib/config.ts
6974
6945
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, renameSync, chmodSync } from "node:fs";
6975
6946
  import { homedir } from "node:os";
@@ -7166,20 +7137,6 @@ async function createEndpoint(token, input) {
7166
7137
  });
7167
7138
  return result;
7168
7139
  }
7169
- async function exchangeCliCode(sessionId, code) {
7170
- const apiUrl = getApiUrl();
7171
- const response = await fetch(`${apiUrl}/api/auth/cli-exchange`, {
7172
- method: "POST",
7173
- headers: { "Content-Type": "application/json" },
7174
- body: JSON.stringify({ sessionId, code }),
7175
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
7176
- });
7177
- const json = await response.json();
7178
- if (!response.ok || !json.success) {
7179
- throw new ApiError(response.status, json.error?.code ?? "UNKNOWN_ERROR", json.error?.message ?? "Failed to exchange code");
7180
- }
7181
- return json.data;
7182
- }
7183
7140
  async function revokeCliToken(token) {
7184
7141
  const apiUrl = getApiUrl();
7185
7142
  try {
@@ -8555,18 +8512,130 @@ function createSpinner(text) {
8555
8512
  }
8556
8513
 
8557
8514
  // src/commands/login.ts
8558
- function prompt(question) {
8559
- const rl = createInterface({
8560
- input: process.stdin,
8561
- output: process.stdout
8562
- });
8515
+ var DEFAULT_PORT = 9847;
8516
+ var PORT_RANGE_START = 9800;
8517
+ var PORT_RANGE_END = 9900;
8518
+ var AUTH_TIMEOUT_MS = 2 * 60 * 1000;
8519
+ function tryPort(port) {
8563
8520
  return new Promise((resolve) => {
8564
- rl.question(question, (answer) => {
8565
- rl.close();
8566
- resolve(answer.trim());
8521
+ const server = createServer();
8522
+ server.once("error", (err) => {
8523
+ if (err.code === "EADDRINUSE") {
8524
+ resolve(null);
8525
+ } else {
8526
+ resolve(null);
8527
+ }
8528
+ });
8529
+ server.listen(port, "127.0.0.1", () => {
8530
+ resolve(server);
8567
8531
  });
8568
8532
  });
8569
8533
  }
8534
+ async function findAvailablePort() {
8535
+ const defaultServer = await tryPort(DEFAULT_PORT);
8536
+ if (defaultServer) {
8537
+ return { server: defaultServer, port: DEFAULT_PORT };
8538
+ }
8539
+ for (let i = 0;i < 10; i++) {
8540
+ const port = PORT_RANGE_START + Math.floor(Math.random() * (PORT_RANGE_END - PORT_RANGE_START));
8541
+ const server = await tryPort(port);
8542
+ if (server) {
8543
+ return { server, port };
8544
+ }
8545
+ }
8546
+ return null;
8547
+ }
8548
+ function generateState() {
8549
+ return randomBytes2(16).toString("hex");
8550
+ }
8551
+ var SUCCESS_HTML = `<!DOCTYPE html>
8552
+ <html>
8553
+ <head>
8554
+ <title>Authenticated - nbound</title>
8555
+ <style>
8556
+ body {
8557
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8558
+ display: flex;
8559
+ justify-content: center;
8560
+ align-items: center;
8561
+ min-height: 100vh;
8562
+ margin: 0;
8563
+ background: #0a0a0a;
8564
+ color: #fafafa;
8565
+ }
8566
+ .container {
8567
+ text-align: center;
8568
+ padding: 2rem;
8569
+ }
8570
+ .icon {
8571
+ font-size: 4rem;
8572
+ margin-bottom: 1rem;
8573
+ }
8574
+ h1 {
8575
+ font-size: 1.5rem;
8576
+ font-weight: 600;
8577
+ margin: 0 0 0.5rem;
8578
+ }
8579
+ p {
8580
+ color: #a1a1aa;
8581
+ margin: 0;
8582
+ }
8583
+ </style>
8584
+ </head>
8585
+ <body>
8586
+ <div class="container">
8587
+ <div class="icon">&#10003;</div>
8588
+ <h1>Authenticated!</h1>
8589
+ <p>You can close this tab and return to your terminal.</p>
8590
+ </div>
8591
+ <script>window.close()</script>
8592
+ </body>
8593
+ </html>`;
8594
+ function errorHtml(message) {
8595
+ return `<!DOCTYPE html>
8596
+ <html>
8597
+ <head>
8598
+ <title>Authentication Failed - nbound</title>
8599
+ <style>
8600
+ body {
8601
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8602
+ display: flex;
8603
+ justify-content: center;
8604
+ align-items: center;
8605
+ min-height: 100vh;
8606
+ margin: 0;
8607
+ background: #0a0a0a;
8608
+ color: #fafafa;
8609
+ }
8610
+ .container {
8611
+ text-align: center;
8612
+ padding: 2rem;
8613
+ }
8614
+ .icon {
8615
+ font-size: 4rem;
8616
+ margin-bottom: 1rem;
8617
+ color: #ef4444;
8618
+ }
8619
+ h1 {
8620
+ font-size: 1.5rem;
8621
+ font-weight: 600;
8622
+ margin: 0 0 0.5rem;
8623
+ }
8624
+ p {
8625
+ color: #a1a1aa;
8626
+ margin: 0;
8627
+ }
8628
+ </style>
8629
+ </head>
8630
+ <body>
8631
+ <div class="container">
8632
+ <div class="icon">&#10007;</div>
8633
+ <h1>Authentication Failed</h1>
8634
+ <p>${message}</p>
8635
+ </div>
8636
+ </body>
8637
+ </html>`;
8638
+ }
8570
8639
  async function loginCommand(options) {
8571
8640
  const existingConfig = loadConfig();
8572
8641
  if (existingConfig && !options.token) {
@@ -8576,15 +8645,15 @@ async function loginCommand(options) {
8576
8645
  const expiry = checkTokenExpiry(existingConfig);
8577
8646
  if (expiry === "expiring_soon") {
8578
8647
  const daysLeft = getDaysUntilExpiry(existingConfig);
8579
- log.warning(`Your token expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"}. Run: nbound login to refresh.`);
8648
+ log.warning(`Your token expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"}. Run: nbound logout && nbound login`);
8580
8649
  }
8581
8650
  log.dim("Run: nbound logout to switch accounts");
8582
8651
  return;
8583
8652
  } catch {}
8584
8653
  }
8585
8654
  if (options.token) {
8586
- const spinner2 = createSpinner("Validating token...");
8587
- spinner2.start();
8655
+ const spinner = createSpinner("Validating token...");
8656
+ spinner.start();
8588
8657
  try {
8589
8658
  const user = await getUser(options.token);
8590
8659
  const apiUrl2 = getApiUrl();
@@ -8594,21 +8663,70 @@ async function loginCommand(options) {
8594
8663
  apiUrl: apiUrl2,
8595
8664
  wsUrl: apiUrl2.replace("https://", "wss://").replace("http://", "ws://") + "/ws"
8596
8665
  });
8597
- spinner2.succeed(`Logged in as ${user.email}`);
8666
+ spinner.succeed(`Logged in as ${user.email}`);
8598
8667
  } catch (error) {
8599
- spinner2.fail("Invalid token");
8668
+ spinner.fail("Invalid token");
8600
8669
  if (error instanceof ApiError) {
8601
8670
  log.error(error.message);
8602
8671
  }
8603
- process.exit(2);
8672
+ process.exit(1);
8604
8673
  }
8605
8674
  return;
8606
8675
  }
8607
- const sessionId = nanoid(21);
8608
- const apiUrl = getApiUrl();
8609
- const appUrl = apiUrl.replace("://api.", "://app.");
8610
- const authUrl = `${appUrl}/cli/auth?session=${sessionId}`;
8611
8676
  log.blank();
8677
+ const portResult = await findAvailablePort();
8678
+ if (!portResult) {
8679
+ log.error("Could not find an available port for authentication callback.");
8680
+ log.dim("Try closing other applications or run: nbound login --token <token>");
8681
+ process.exit(1);
8682
+ }
8683
+ const { server, port } = portResult;
8684
+ const state = generateState();
8685
+ const apiUrl = getApiUrl();
8686
+ const redirectUrl = `http://localhost:${port}/callback`;
8687
+ const authUrl = `${apiUrl}/api/cli/auth?redirect=${encodeURIComponent(redirectUrl)}&state=${state}`;
8688
+ const timeoutHandle = setTimeout(() => {
8689
+ server.close();
8690
+ log.blank();
8691
+ log.error("Authentication timed out after 2 minutes.");
8692
+ log.dim("Please try again: nbound login");
8693
+ process.exit(1);
8694
+ }, AUTH_TIMEOUT_MS);
8695
+ const authPromise = new Promise((resolve, reject) => {
8696
+ server.on("request", (req, res) => {
8697
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
8698
+ if (url.pathname !== "/callback") {
8699
+ res.writeHead(404);
8700
+ res.end("Not found");
8701
+ return;
8702
+ }
8703
+ const receivedState = url.searchParams.get("state");
8704
+ const token = url.searchParams.get("token");
8705
+ const error = url.searchParams.get("error");
8706
+ const expiresAt = url.searchParams.get("expiresAt");
8707
+ if (error) {
8708
+ res.writeHead(200, { "Content-Type": "text/html" });
8709
+ res.end(errorHtml(error));
8710
+ reject(new Error(error));
8711
+ return;
8712
+ }
8713
+ if (receivedState !== state) {
8714
+ res.writeHead(200, { "Content-Type": "text/html" });
8715
+ res.end(errorHtml("Security validation failed. Please try again."));
8716
+ reject(new Error("State mismatch - possible CSRF attack"));
8717
+ return;
8718
+ }
8719
+ if (!token) {
8720
+ res.writeHead(200, { "Content-Type": "text/html" });
8721
+ res.end(errorHtml("No authentication token received."));
8722
+ reject(new Error("No token received"));
8723
+ return;
8724
+ }
8725
+ res.writeHead(200, { "Content-Type": "text/html" });
8726
+ res.end(SUCCESS_HTML);
8727
+ resolve({ token, expiresAt: expiresAt || undefined });
8728
+ });
8729
+ });
8612
8730
  log.info("Opening browser to authenticate...");
8613
8731
  log.dim(authUrl);
8614
8732
  log.blank();
@@ -8619,31 +8737,37 @@ async function loginCommand(options) {
8619
8737
  log.info("Please open this URL in your browser:");
8620
8738
  log.info(authUrl);
8621
8739
  }
8622
- log.blank();
8623
- const code = await prompt("Enter the code from the browser: ");
8624
- if (!code) {
8625
- log.error("No code provided");
8626
- process.exit(4);
8627
- }
8628
- const spinner = createSpinner("Verifying code...");
8629
- spinner.start();
8740
+ log.dim("Waiting for authentication...");
8630
8741
  try {
8631
- const { token, user, expiresAt } = await exchangeCliCode(sessionId, code);
8632
- saveConfig({
8633
- token,
8634
- user: { id: user.id, email: user.email },
8635
- apiUrl,
8636
- wsUrl: apiUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws",
8637
- expiresAt
8638
- });
8639
- spinner.succeed(`Logged in as ${user.email}`);
8640
- log.blank();
8641
- } catch (error) {
8642
- spinner.fail("Failed to verify code");
8643
- if (error instanceof ApiError) {
8644
- log.error(error.message);
8742
+ const { token, expiresAt } = await authPromise;
8743
+ clearTimeout(timeoutHandle);
8744
+ server.close();
8745
+ const spinner = createSpinner("Verifying token...");
8746
+ spinner.start();
8747
+ try {
8748
+ const user = await getUser(token);
8749
+ saveConfig({
8750
+ token,
8751
+ user: { id: user.id, email: user.email },
8752
+ apiUrl,
8753
+ wsUrl: apiUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws",
8754
+ expiresAt
8755
+ });
8756
+ spinner.succeed(`Logged in as ${user.email}`);
8757
+ log.blank();
8758
+ } catch (error) {
8759
+ spinner.fail("Failed to verify token");
8760
+ if (error instanceof ApiError) {
8761
+ log.error(error.message);
8762
+ }
8763
+ process.exit(1);
8645
8764
  }
8646
- process.exit(2);
8765
+ } catch (error) {
8766
+ clearTimeout(timeoutHandle);
8767
+ server.close();
8768
+ log.blank();
8769
+ log.error(error instanceof Error ? error.message : "Authentication failed");
8770
+ process.exit(1);
8647
8771
  }
8648
8772
  }
8649
8773
 
@@ -12796,7 +12920,7 @@ function getBackoffDelay(attempt, baseMs = 1000, maxMs = 30000, jitter = true) {
12796
12920
  // src/lib/ws-client.ts
12797
12921
  var PING_INTERVAL_MS = 30000;
12798
12922
  var PONG_TIMEOUT_MS = 1e4;
12799
- var AUTH_TIMEOUT_MS = 1e4;
12923
+ var AUTH_TIMEOUT_MS2 = 1e4;
12800
12924
 
12801
12925
  class WebSocketClient {
12802
12926
  ws = null;
@@ -12851,10 +12975,15 @@ class WebSocketClient {
12851
12975
  this.options.onError(new Error(`Connection closed during auth: ${reasonStr}`));
12852
12976
  } else if (this.shouldReconnect && prevState === "connecting") {
12853
12977
  this.scheduleReconnect();
12978
+ } else if (this.shouldReconnect && prevState === "authenticated") {
12979
+ this.scheduleReconnect();
12854
12980
  }
12855
12981
  });
12856
12982
  this.ws.on("error", (error) => {
12857
12983
  if (this.state !== "subscribed" && this.shouldReconnect) {
12984
+ if (this.state === "authenticated" || this.state === "authenticating") {
12985
+ this.options.onDisconnected(`Connection error: ${error.message}`);
12986
+ }
12858
12987
  this.scheduleReconnect();
12859
12988
  } else {
12860
12989
  this.options.onError(error);
@@ -12909,6 +13038,12 @@ class WebSocketClient {
12909
13038
  this.disconnect();
12910
13039
  break;
12911
13040
  }
13041
+ case "session_replaced": {
13042
+ this.shouldReconnect = false;
13043
+ this.options.onError(new Error("Another CLI session connected to this endpoint."));
13044
+ this.disconnect();
13045
+ break;
13046
+ }
12912
13047
  case "plan_changed": {
12913
13048
  const payload = message.payload;
12914
13049
  this.plan = payload.plan;
@@ -13014,7 +13149,7 @@ class WebSocketClient {
13014
13149
  this.options.onError(new Error("Authentication timed out"));
13015
13150
  this.disconnect();
13016
13151
  }
13017
- }, AUTH_TIMEOUT_MS);
13152
+ }, AUTH_TIMEOUT_MS2);
13018
13153
  }
13019
13154
  clearAuthTimeout() {
13020
13155
  if (this.authTimeout) {
@@ -13580,7 +13715,7 @@ async function createCommand2(nameArg, options) {
13580
13715
  }
13581
13716
 
13582
13717
  // src/index.ts
13583
- program.name("nbound").description("Forward webhooks to localhost for development").version("1.0.0");
13718
+ program.name("nbound").description("Forward webhooks to localhost for development").version("1.1.0");
13584
13719
  program.command("login").description("Authenticate with nbound").option("--token <token>", "Use an existing API token").action(loginCommand);
13585
13720
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
13586
13721
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);