nbound 1.0.6 → 1.1.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.
@@ -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,232 @@ 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
+ * { box-sizing: border-box; }
8557
+ body {
8558
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8559
+ display: flex;
8560
+ justify-content: center;
8561
+ align-items: center;
8562
+ min-height: 100vh;
8563
+ margin: 0;
8564
+ background: #0a0a0a;
8565
+ color: #fafafa;
8566
+ }
8567
+ .container {
8568
+ text-align: center;
8569
+ padding: 2rem;
8570
+ animation: fadeIn 0.3s ease-out;
8571
+ }
8572
+ @keyframes fadeIn {
8573
+ from { opacity: 0; transform: translateY(-10px); }
8574
+ to { opacity: 1; transform: translateY(0); }
8575
+ }
8576
+ @keyframes checkmark {
8577
+ 0% { transform: scale(0); }
8578
+ 50% { transform: scale(1.2); }
8579
+ 100% { transform: scale(1); }
8580
+ }
8581
+ .icon-wrapper {
8582
+ width: 80px;
8583
+ height: 80px;
8584
+ margin: 0 auto 1.5rem;
8585
+ background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
8586
+ border-radius: 50%;
8587
+ display: flex;
8588
+ align-items: center;
8589
+ justify-content: center;
8590
+ animation: checkmark 0.4s ease-out;
8591
+ }
8592
+ .icon {
8593
+ font-size: 2.5rem;
8594
+ color: white;
8595
+ line-height: 1;
8596
+ }
8597
+ .logo {
8598
+ font-size: 0.875rem;
8599
+ font-weight: 600;
8600
+ letter-spacing: 0.05em;
8601
+ color: #71717a;
8602
+ margin-bottom: 1.5rem;
8603
+ text-transform: uppercase;
8604
+ }
8605
+ h1 {
8606
+ font-size: 1.75rem;
8607
+ font-weight: 600;
8608
+ margin: 0 0 0.5rem;
8609
+ }
8610
+ p {
8611
+ color: #a1a1aa;
8612
+ margin: 0;
8613
+ font-size: 1rem;
8614
+ }
8615
+ .hint {
8616
+ margin-top: 2rem;
8617
+ padding: 0.75rem 1.5rem;
8618
+ background: #18181b;
8619
+ border-radius: 8px;
8620
+ border: 1px solid #27272a;
8621
+ color: #71717a;
8622
+ font-size: 0.875rem;
8623
+ }
8624
+ .hint kbd {
8625
+ background: #27272a;
8626
+ padding: 0.125rem 0.375rem;
8627
+ border-radius: 4px;
8628
+ font-family: ui-monospace, monospace;
8629
+ font-size: 0.8rem;
8630
+ color: #a1a1aa;
8631
+ }
8632
+ </style>
8633
+ </head>
8634
+ <body>
8635
+ <div class="container">
8636
+ <div class="logo">nbound</div>
8637
+ <div class="icon-wrapper">
8638
+ <div class="icon">&#10003;</div>
8639
+ </div>
8640
+ <h1>You're in!</h1>
8641
+ <p>Authentication successful. Return to your terminal.</p>
8642
+ <div class="hint">Press <kbd>Cmd</kbd> + <kbd>Tab</kbd> to switch back</div>
8643
+ </div>
8644
+ <script>setTimeout(() => window.close(), 1500)</script>
8645
+ </body>
8646
+ </html>`;
8647
+ function errorHtml(message) {
8648
+ return `<!DOCTYPE html>
8649
+ <html>
8650
+ <head>
8651
+ <title>Authentication Failed - nbound</title>
8652
+ <style>
8653
+ * { box-sizing: border-box; }
8654
+ body {
8655
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8656
+ display: flex;
8657
+ justify-content: center;
8658
+ align-items: center;
8659
+ min-height: 100vh;
8660
+ margin: 0;
8661
+ background: #0a0a0a;
8662
+ color: #fafafa;
8663
+ }
8664
+ .container {
8665
+ text-align: center;
8666
+ padding: 2rem;
8667
+ animation: fadeIn 0.3s ease-out;
8668
+ }
8669
+ @keyframes fadeIn {
8670
+ from { opacity: 0; transform: translateY(-10px); }
8671
+ to { opacity: 1; transform: translateY(0); }
8672
+ }
8673
+ @keyframes shake {
8674
+ 0%, 100% { transform: translateX(0); }
8675
+ 20%, 60% { transform: translateX(-5px); }
8676
+ 40%, 80% { transform: translateX(5px); }
8677
+ }
8678
+ .icon-wrapper {
8679
+ width: 80px;
8680
+ height: 80px;
8681
+ margin: 0 auto 1.5rem;
8682
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
8683
+ border-radius: 50%;
8684
+ display: flex;
8685
+ align-items: center;
8686
+ justify-content: center;
8687
+ animation: shake 0.4s ease-out;
8688
+ }
8689
+ .icon {
8690
+ font-size: 2.5rem;
8691
+ color: white;
8692
+ line-height: 1;
8693
+ }
8694
+ .logo {
8695
+ font-size: 0.875rem;
8696
+ font-weight: 600;
8697
+ letter-spacing: 0.05em;
8698
+ color: #71717a;
8699
+ margin-bottom: 1.5rem;
8700
+ text-transform: uppercase;
8701
+ }
8702
+ h1 {
8703
+ font-size: 1.75rem;
8704
+ font-weight: 600;
8705
+ margin: 0 0 0.5rem;
8706
+ }
8707
+ p {
8708
+ color: #a1a1aa;
8709
+ margin: 0;
8710
+ font-size: 1rem;
8711
+ max-width: 300px;
8712
+ }
8713
+ .hint {
8714
+ margin-top: 2rem;
8715
+ padding: 0.75rem 1.5rem;
8716
+ background: #18181b;
8717
+ border-radius: 8px;
8718
+ border: 1px solid #27272a;
8719
+ color: #71717a;
8720
+ font-size: 0.875rem;
8721
+ }
8722
+ .hint code {
8723
+ color: #a1a1aa;
8724
+ font-family: ui-monospace, monospace;
8725
+ }
8726
+ </style>
8727
+ </head>
8728
+ <body>
8729
+ <div class="container">
8730
+ <div class="logo">nbound</div>
8731
+ <div class="icon-wrapper">
8732
+ <div class="icon">&#10007;</div>
8733
+ </div>
8734
+ <h1>Authentication Failed</h1>
8735
+ <p>${message}</p>
8736
+ <div class="hint">Try again: <code>nbound login</code></div>
8737
+ </div>
8738
+ </body>
8739
+ </html>`;
8740
+ }
8570
8741
  async function loginCommand(options) {
8571
8742
  const existingConfig = loadConfig();
8572
8743
  if (existingConfig && !options.token) {
@@ -8576,15 +8747,15 @@ async function loginCommand(options) {
8576
8747
  const expiry = checkTokenExpiry(existingConfig);
8577
8748
  if (expiry === "expiring_soon") {
8578
8749
  const daysLeft = getDaysUntilExpiry(existingConfig);
8579
- log.warning(`Your token expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"}. Run: nbound login to refresh.`);
8750
+ log.warning(`Your token expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"}. Run: nbound logout && nbound login`);
8580
8751
  }
8581
8752
  log.dim("Run: nbound logout to switch accounts");
8582
8753
  return;
8583
8754
  } catch {}
8584
8755
  }
8585
8756
  if (options.token) {
8586
- const spinner2 = createSpinner("Validating token...");
8587
- spinner2.start();
8757
+ const spinner = createSpinner("Validating token...");
8758
+ spinner.start();
8588
8759
  try {
8589
8760
  const user = await getUser(options.token);
8590
8761
  const apiUrl2 = getApiUrl();
@@ -8594,21 +8765,70 @@ async function loginCommand(options) {
8594
8765
  apiUrl: apiUrl2,
8595
8766
  wsUrl: apiUrl2.replace("https://", "wss://").replace("http://", "ws://") + "/ws"
8596
8767
  });
8597
- spinner2.succeed(`Logged in as ${user.email}`);
8768
+ spinner.succeed(`Logged in as ${user.email}`);
8598
8769
  } catch (error) {
8599
- spinner2.fail("Invalid token");
8770
+ spinner.fail("Invalid token");
8600
8771
  if (error instanceof ApiError) {
8601
8772
  log.error(error.message);
8602
8773
  }
8603
- process.exit(2);
8774
+ process.exit(1);
8604
8775
  }
8605
8776
  return;
8606
8777
  }
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
8778
  log.blank();
8779
+ const portResult = await findAvailablePort();
8780
+ if (!portResult) {
8781
+ log.error("Could not find an available port for authentication callback.");
8782
+ log.dim("Try closing other applications or run: nbound login --token <token>");
8783
+ process.exit(1);
8784
+ }
8785
+ const { server, port } = portResult;
8786
+ const state = generateState();
8787
+ const apiUrl = getApiUrl();
8788
+ const redirectUrl = `http://localhost:${port}/callback`;
8789
+ const authUrl = `${apiUrl}/api/cli/auth?redirect=${encodeURIComponent(redirectUrl)}&state=${state}`;
8790
+ const timeoutHandle = setTimeout(() => {
8791
+ server.close();
8792
+ log.blank();
8793
+ log.error("Authentication timed out after 2 minutes.");
8794
+ log.dim("Please try again: nbound login");
8795
+ process.exit(1);
8796
+ }, AUTH_TIMEOUT_MS);
8797
+ const authPromise = new Promise((resolve, reject) => {
8798
+ server.on("request", (req, res) => {
8799
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
8800
+ if (url.pathname !== "/callback") {
8801
+ res.writeHead(404);
8802
+ res.end("Not found");
8803
+ return;
8804
+ }
8805
+ const receivedState = url.searchParams.get("state");
8806
+ const token = url.searchParams.get("token");
8807
+ const error = url.searchParams.get("error");
8808
+ const expiresAt = url.searchParams.get("expiresAt");
8809
+ if (error) {
8810
+ res.writeHead(200, { "Content-Type": "text/html" });
8811
+ res.end(errorHtml(error));
8812
+ reject(new Error(error));
8813
+ return;
8814
+ }
8815
+ if (receivedState !== state) {
8816
+ res.writeHead(200, { "Content-Type": "text/html" });
8817
+ res.end(errorHtml("Security validation failed. Please try again."));
8818
+ reject(new Error("State mismatch - possible CSRF attack"));
8819
+ return;
8820
+ }
8821
+ if (!token) {
8822
+ res.writeHead(200, { "Content-Type": "text/html" });
8823
+ res.end(errorHtml("No authentication token received."));
8824
+ reject(new Error("No token received"));
8825
+ return;
8826
+ }
8827
+ res.writeHead(200, { "Content-Type": "text/html" });
8828
+ res.end(SUCCESS_HTML);
8829
+ resolve({ token, expiresAt: expiresAt || undefined });
8830
+ });
8831
+ });
8612
8832
  log.info("Opening browser to authenticate...");
8613
8833
  log.dim(authUrl);
8614
8834
  log.blank();
@@ -8619,31 +8839,37 @@ async function loginCommand(options) {
8619
8839
  log.info("Please open this URL in your browser:");
8620
8840
  log.info(authUrl);
8621
8841
  }
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();
8842
+ log.dim("Waiting for authentication...");
8630
8843
  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);
8844
+ const { token, expiresAt } = await authPromise;
8845
+ clearTimeout(timeoutHandle);
8846
+ server.close();
8847
+ const spinner = createSpinner("Verifying token...");
8848
+ spinner.start();
8849
+ try {
8850
+ const user = await getUser(token);
8851
+ saveConfig({
8852
+ token,
8853
+ user: { id: user.id, email: user.email },
8854
+ apiUrl,
8855
+ wsUrl: apiUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws",
8856
+ expiresAt
8857
+ });
8858
+ spinner.succeed(`Logged in as ${user.email}`);
8859
+ log.blank();
8860
+ } catch (error) {
8861
+ spinner.fail("Failed to verify token");
8862
+ if (error instanceof ApiError) {
8863
+ log.error(error.message);
8864
+ }
8865
+ process.exit(1);
8645
8866
  }
8646
- process.exit(2);
8867
+ } catch (error) {
8868
+ clearTimeout(timeoutHandle);
8869
+ server.close();
8870
+ log.blank();
8871
+ log.error(error instanceof Error ? error.message : "Authentication failed");
8872
+ process.exit(1);
8647
8873
  }
8648
8874
  }
8649
8875
 
@@ -12796,7 +13022,7 @@ function getBackoffDelay(attempt, baseMs = 1000, maxMs = 30000, jitter = true) {
12796
13022
  // src/lib/ws-client.ts
12797
13023
  var PING_INTERVAL_MS = 30000;
12798
13024
  var PONG_TIMEOUT_MS = 1e4;
12799
- var AUTH_TIMEOUT_MS = 1e4;
13025
+ var AUTH_TIMEOUT_MS2 = 1e4;
12800
13026
 
12801
13027
  class WebSocketClient {
12802
13028
  ws = null;
@@ -12851,10 +13077,15 @@ class WebSocketClient {
12851
13077
  this.options.onError(new Error(`Connection closed during auth: ${reasonStr}`));
12852
13078
  } else if (this.shouldReconnect && prevState === "connecting") {
12853
13079
  this.scheduleReconnect();
13080
+ } else if (this.shouldReconnect && prevState === "authenticated") {
13081
+ this.scheduleReconnect();
12854
13082
  }
12855
13083
  });
12856
13084
  this.ws.on("error", (error) => {
12857
13085
  if (this.state !== "subscribed" && this.shouldReconnect) {
13086
+ if (this.state === "authenticated" || this.state === "authenticating") {
13087
+ this.options.onDisconnected(`Connection error: ${error.message}`);
13088
+ }
12858
13089
  this.scheduleReconnect();
12859
13090
  } else {
12860
13091
  this.options.onError(error);
@@ -12909,6 +13140,12 @@ class WebSocketClient {
12909
13140
  this.disconnect();
12910
13141
  break;
12911
13142
  }
13143
+ case "session_replaced": {
13144
+ this.shouldReconnect = false;
13145
+ this.options.onError(new Error("Another CLI session connected to this endpoint."));
13146
+ this.disconnect();
13147
+ break;
13148
+ }
12912
13149
  case "plan_changed": {
12913
13150
  const payload = message.payload;
12914
13151
  this.plan = payload.plan;
@@ -13014,7 +13251,7 @@ class WebSocketClient {
13014
13251
  this.options.onError(new Error("Authentication timed out"));
13015
13252
  this.disconnect();
13016
13253
  }
13017
- }, AUTH_TIMEOUT_MS);
13254
+ }, AUTH_TIMEOUT_MS2);
13018
13255
  }
13019
13256
  clearAuthTimeout() {
13020
13257
  if (this.authTimeout) {
@@ -13580,7 +13817,7 @@ async function createCommand2(nameArg, options) {
13580
13817
  }
13581
13818
 
13582
13819
  // src/index.ts
13583
- program.name("nbound").description("Forward webhooks to localhost for development").version("1.0.6");
13820
+ program.name("nbound").description("Forward webhooks to localhost for development").version("1.1.0");
13584
13821
  program.command("login").description("Authenticate with nbound").option("--token <token>", "Use an existing API token").action(loginCommand);
13585
13822
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
13586
13823
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);