nbound 1.0.2 → 1.0.4

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/dist/index.js CHANGED
@@ -5221,7 +5221,7 @@ var require_websocket = __commonJS((exports, module) => {
5221
5221
  var http = __require("http");
5222
5222
  var net = __require("net");
5223
5223
  var tls = __require("tls");
5224
- var { randomBytes, createHash } = __require("crypto");
5224
+ var { randomBytes: randomBytes2, createHash } = __require("crypto");
5225
5225
  var { Duplex, Readable } = __require("stream");
5226
5226
  var { URL: URL2 } = __require("url");
5227
5227
  var PerMessageDeflate = require_permessage_deflate();
@@ -5630,7 +5630,7 @@ var require_websocket = __commonJS((exports, module) => {
5630
5630
  }
5631
5631
  }
5632
5632
  const defaultPort = isSecure ? 443 : 80;
5633
- const key = randomBytes(16).toString("base64");
5633
+ const key = randomBytes2(16).toString("base64");
5634
5634
  const request2 = isSecure ? https.request : http.request;
5635
5635
  const protocolSet = new Set;
5636
5636
  let perMessageDeflate;
@@ -6969,17 +6969,45 @@ function nanoid(size = 21) {
6969
6969
  }
6970
6970
 
6971
6971
  // src/lib/config.ts
6972
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
6972
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, renameSync, chmodSync } from "node:fs";
6973
6973
  import { homedir } from "node:os";
6974
6974
  import { join } from "node:path";
6975
+ import { randomBytes } from "node:crypto";
6975
6976
  var CONFIG_DIR = ".nbound";
6976
6977
  var CONFIG_FILE = "config.json";
6978
+ var EXPIRY_WARNING_MS = 7 * 24 * 60 * 60 * 1000;
6977
6979
  function getConfigDir() {
6978
6980
  return join(homedir(), CONFIG_DIR);
6979
6981
  }
6980
6982
  function getConfigPath() {
6981
6983
  return join(getConfigDir(), CONFIG_FILE);
6982
6984
  }
6985
+ function checkTokenExpiry(config) {
6986
+ if (!config.expiresAt) {
6987
+ return "valid";
6988
+ }
6989
+ const expiresAt = new Date(config.expiresAt).getTime();
6990
+ const now = Date.now();
6991
+ if (now >= expiresAt) {
6992
+ return "expired";
6993
+ }
6994
+ if (expiresAt - now <= EXPIRY_WARNING_MS) {
6995
+ return "expiring_soon";
6996
+ }
6997
+ return "valid";
6998
+ }
6999
+ function getDaysUntilExpiry(config) {
7000
+ if (!config.expiresAt) {
7001
+ return null;
7002
+ }
7003
+ const expiresAt = new Date(config.expiresAt).getTime();
7004
+ const now = Date.now();
7005
+ const msRemaining = expiresAt - now;
7006
+ if (msRemaining <= 0) {
7007
+ return 0;
7008
+ }
7009
+ return Math.ceil(msRemaining / (24 * 60 * 60 * 1000));
7010
+ }
6983
7011
  function loadConfig() {
6984
7012
  const envToken = process.env.NBOUND_TOKEN;
6985
7013
  if (envToken) {
@@ -6997,7 +7025,12 @@ function loadConfig() {
6997
7025
  }
6998
7026
  try {
6999
7027
  const content = readFileSync(configPath, "utf-8");
7000
- return JSON.parse(content);
7028
+ const config = JSON.parse(content);
7029
+ const expiry = checkTokenExpiry(config);
7030
+ if (expiry === "expired") {
7031
+ return null;
7032
+ }
7033
+ return config;
7001
7034
  } catch {
7002
7035
  return null;
7003
7036
  }
@@ -7005,10 +7038,30 @@ function loadConfig() {
7005
7038
  function saveConfig(config) {
7006
7039
  const configDir = getConfigDir();
7007
7040
  const configPath = getConfigPath();
7008
- if (!existsSync(configDir)) {
7009
- mkdirSync(configDir, { recursive: true, mode: 448 });
7041
+ try {
7042
+ if (!existsSync(configDir)) {
7043
+ mkdirSync(configDir, { recursive: true, mode: 448 });
7044
+ } else {
7045
+ chmodSync(configDir, 448);
7046
+ }
7047
+ } catch (err) {
7048
+ if (!existsSync(configDir)) {
7049
+ throw err;
7050
+ }
7051
+ }
7052
+ const tempPath = join(configDir, `.config.${randomBytes(8).toString("hex")}.tmp`);
7053
+ try {
7054
+ writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 384 });
7055
+ chmodSync(tempPath, 384);
7056
+ renameSync(tempPath, configPath);
7057
+ } catch (err) {
7058
+ try {
7059
+ if (existsSync(tempPath)) {
7060
+ unlinkSync(tempPath);
7061
+ }
7062
+ } catch {}
7063
+ throw err;
7010
7064
  }
7011
- writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
7012
7065
  }
7013
7066
  function deleteConfig() {
7014
7067
  const configPath = getConfigPath();
@@ -7030,6 +7083,10 @@ function getWsUrl() {
7030
7083
  }
7031
7084
 
7032
7085
  // src/lib/api.ts
7086
+ var REQUEST_TIMEOUT_MS = 1e4;
7087
+ var MAX_RETRIES = 3;
7088
+ var RETRY_DELAYS = [1000, 2000, 4000];
7089
+
7033
7090
  class ApiError extends Error {
7034
7091
  status;
7035
7092
  code;
@@ -7040,22 +7097,57 @@ class ApiError extends Error {
7040
7097
  this.name = "ApiError";
7041
7098
  }
7042
7099
  }
7100
+ function sleep(ms) {
7101
+ return new Promise((resolve) => setTimeout(resolve, ms));
7102
+ }
7103
+ function isRetryable(status) {
7104
+ return status >= 500 && status < 600;
7105
+ }
7043
7106
  async function request(path2, token, options = {}) {
7044
7107
  const apiUrl = getApiUrl();
7045
7108
  const url = `${apiUrl}${path2}`;
7046
- const response = await fetch(url, {
7047
- ...options,
7048
- headers: {
7049
- "Content-Type": "application/json",
7050
- Authorization: `Bearer ${token}`,
7051
- ...options.headers
7109
+ let lastError = null;
7110
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
7111
+ try {
7112
+ const response = await fetch(url, {
7113
+ ...options,
7114
+ headers: {
7115
+ "Content-Type": "application/json",
7116
+ Authorization: `Bearer ${token}`,
7117
+ ...options.headers
7118
+ },
7119
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
7120
+ });
7121
+ const json = await response.json();
7122
+ if (!response.ok || !json.success) {
7123
+ const error = new ApiError(response.status, json.error?.code ?? "UNKNOWN_ERROR", json.error?.message ?? "An unknown error occurred");
7124
+ if (isRetryable(response.status) && attempt < MAX_RETRIES) {
7125
+ lastError = error;
7126
+ await sleep(RETRY_DELAYS[attempt]);
7127
+ continue;
7128
+ }
7129
+ throw error;
7130
+ }
7131
+ return json.data;
7132
+ } catch (error) {
7133
+ if (error instanceof ApiError && !isRetryable(error.status)) {
7134
+ throw error;
7135
+ }
7136
+ if (attempt < MAX_RETRIES) {
7137
+ lastError = error instanceof Error ? error : new Error(String(error));
7138
+ await sleep(RETRY_DELAYS[attempt]);
7139
+ continue;
7140
+ }
7141
+ if (error instanceof Error) {
7142
+ if (error.name === "TimeoutError" || error.name === "AbortError") {
7143
+ throw new ApiError(0, "TIMEOUT", `Request timed out after ${REQUEST_TIMEOUT_MS}ms`);
7144
+ }
7145
+ throw new ApiError(0, "NETWORK_ERROR", error.message);
7146
+ }
7147
+ throw error;
7052
7148
  }
7053
- });
7054
- const json = await response.json();
7055
- if (!response.ok || !json.success) {
7056
- throw new ApiError(response.status, json.error?.code ?? "UNKNOWN_ERROR", json.error?.message ?? "An unknown error occurred");
7057
7149
  }
7058
- return json.data;
7150
+ throw lastError ?? new ApiError(0, "UNKNOWN_ERROR", "Request failed");
7059
7151
  }
7060
7152
  async function getUser(token) {
7061
7153
  const result = await request("/api/auth/me", token);
@@ -7065,12 +7157,20 @@ async function listEndpoints(token) {
7065
7157
  const result = await request("/api/endpoints", token);
7066
7158
  return result;
7067
7159
  }
7160
+ async function createEndpoint(token, input) {
7161
+ const result = await request("/api/endpoints", token, {
7162
+ method: "POST",
7163
+ body: JSON.stringify(input)
7164
+ });
7165
+ return result;
7166
+ }
7068
7167
  async function exchangeCliCode(sessionId, code) {
7069
7168
  const apiUrl = getApiUrl();
7070
7169
  const response = await fetch(`${apiUrl}/api/auth/cli-exchange`, {
7071
7170
  method: "POST",
7072
7171
  headers: { "Content-Type": "application/json" },
7073
- body: JSON.stringify({ sessionId, code })
7172
+ body: JSON.stringify({ sessionId, code }),
7173
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
7074
7174
  });
7075
7175
  const json = await response.json();
7076
7176
  if (!response.ok || !json.success) {
@@ -7078,6 +7178,22 @@ async function exchangeCliCode(sessionId, code) {
7078
7178
  }
7079
7179
  return json.data;
7080
7180
  }
7181
+ async function revokeCliToken(token) {
7182
+ const apiUrl = getApiUrl();
7183
+ try {
7184
+ const response = await fetch(`${apiUrl}/api/cli/revoke`, {
7185
+ method: "POST",
7186
+ headers: {
7187
+ "Content-Type": "application/json",
7188
+ Authorization: `Bearer ${token}`
7189
+ },
7190
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
7191
+ });
7192
+ if (!response.ok) {
7193
+ return;
7194
+ }
7195
+ } catch {}
7196
+ }
7081
7197
 
7082
7198
  // src/ui/logger.ts
7083
7199
  var import_picocolors = __toESM(require_picocolors(), 1);
@@ -8455,6 +8571,11 @@ async function loginCommand(options) {
8455
8571
  try {
8456
8572
  const user = await getUser(existingConfig.token);
8457
8573
  log.info(`Already logged in as ${user.email}`);
8574
+ const expiry = checkTokenExpiry(existingConfig);
8575
+ if (expiry === "expiring_soon") {
8576
+ const daysLeft = getDaysUntilExpiry(existingConfig);
8577
+ log.warning(`Your token expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"}. Run: nbound login to refresh.`);
8578
+ }
8458
8579
  log.dim("Run: nbound logout to switch accounts");
8459
8580
  return;
8460
8581
  } catch {}
@@ -8505,12 +8626,13 @@ async function loginCommand(options) {
8505
8626
  const spinner = createSpinner("Verifying code...");
8506
8627
  spinner.start();
8507
8628
  try {
8508
- const { token, user } = await exchangeCliCode(sessionId, code);
8629
+ const { token, user, expiresAt } = await exchangeCliCode(sessionId, code);
8509
8630
  saveConfig({
8510
8631
  token,
8511
8632
  user: { id: user.id, email: user.email },
8512
8633
  apiUrl,
8513
- wsUrl: apiUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws"
8634
+ wsUrl: apiUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws",
8635
+ expiresAt
8514
8636
  });
8515
8637
  spinner.succeed(`Logged in as ${user.email}`);
8516
8638
  log.blank();
@@ -8531,6 +8653,12 @@ async function logoutCommand() {
8531
8653
  log.info("Not logged in");
8532
8654
  return;
8533
8655
  }
8656
+ const config = loadConfig();
8657
+ if (config?.token) {
8658
+ try {
8659
+ await revokeCliToken(config.token);
8660
+ } catch {}
8661
+ }
8534
8662
  deleteConfig();
8535
8663
  log.success("Logged out");
8536
8664
  log.dim(`Removed ${configPath}`);
@@ -12665,17 +12793,19 @@ function getBackoffDelay(attempt, baseMs = 1000, maxMs = 30000, jitter = true) {
12665
12793
  }
12666
12794
  // src/lib/ws-client.ts
12667
12795
  var PING_INTERVAL_MS = 30000;
12796
+ var PONG_TIMEOUT_MS = 1e4;
12668
12797
  var AUTH_TIMEOUT_MS = 1e4;
12669
12798
 
12670
12799
  class WebSocketClient {
12671
12800
  ws = null;
12672
12801
  options;
12673
12802
  pingInterval = null;
12803
+ pongTimeout = null;
12674
12804
  authTimeout = null;
12805
+ reconnectTimeout = null;
12675
12806
  reconnectAttempt = 0;
12676
- isConnected = false;
12807
+ state = "disconnected";
12677
12808
  shouldReconnect = true;
12678
- authenticated = false;
12679
12809
  userId = null;
12680
12810
  plan = null;
12681
12811
  constructor(options) {
@@ -12689,11 +12819,13 @@ class WebSocketClient {
12689
12819
  if (this.ws) {
12690
12820
  this.ws.removeAllListeners();
12691
12821
  this.ws.close();
12822
+ this.ws = null;
12692
12823
  }
12693
- this.authenticated = false;
12824
+ this.state = "connecting";
12694
12825
  this.ws = new wrapper_default(this.options.wsUrl);
12695
12826
  this.ws.on("open", () => {
12696
12827
  this.reconnectAttempt = 0;
12828
+ this.state = "authenticating";
12697
12829
  this.sendAuth();
12698
12830
  this.startAuthTimeout();
12699
12831
  });
@@ -12701,20 +12833,26 @@ class WebSocketClient {
12701
12833
  try {
12702
12834
  const message = JSON.parse(data.toString());
12703
12835
  this.handleMessage(message);
12704
- } catch {}
12836
+ } catch (err) {
12837
+ this.options.onError(new Error(`Failed to parse WebSocket message: ${err instanceof Error ? err.message : "Unknown error"}`));
12838
+ }
12705
12839
  });
12706
12840
  this.ws.on("close", (code, reason) => {
12841
+ const prevState = this.state;
12707
12842
  this.cleanup();
12708
12843
  const reasonStr = reason?.toString() || `Code ${code}`;
12709
- if (this.shouldReconnect && this.authenticated) {
12844
+ if (this.shouldReconnect && prevState === "subscribed") {
12710
12845
  this.scheduleReconnect();
12711
12846
  this.options.onDisconnected(reasonStr);
12712
- } else if (!this.authenticated && this.shouldReconnect) {
12713
- this.options.onError(new Error(`Connection closed: ${reasonStr}`));
12847
+ } else if (prevState === "authenticating" && this.shouldReconnect) {
12848
+ this.shouldReconnect = false;
12849
+ this.options.onError(new Error(`Connection closed during auth: ${reasonStr}`));
12850
+ } else if (this.shouldReconnect && prevState === "connecting") {
12851
+ this.scheduleReconnect();
12714
12852
  }
12715
12853
  });
12716
12854
  this.ws.on("error", (error) => {
12717
- if (!this.isConnected && this.shouldReconnect) {
12855
+ if (this.state !== "subscribed" && this.shouldReconnect) {
12718
12856
  this.scheduleReconnect();
12719
12857
  } else {
12720
12858
  this.options.onError(error);
@@ -12725,7 +12863,7 @@ class WebSocketClient {
12725
12863
  switch (message.type) {
12726
12864
  case "auth_success": {
12727
12865
  const payload = message.payload;
12728
- this.authenticated = true;
12866
+ this.state = "authenticated";
12729
12867
  this.userId = payload.user?.id ?? payload.userId ?? null;
12730
12868
  this.plan = payload.plan;
12731
12869
  this.clearAuthTimeout();
@@ -12742,7 +12880,7 @@ class WebSocketClient {
12742
12880
  }
12743
12881
  case "subscribed": {
12744
12882
  const payload = message.payload;
12745
- this.isConnected = true;
12883
+ this.state = "subscribed";
12746
12884
  this.options.onConnected(payload.endpointIds, payload.webhookUrls ?? {});
12747
12885
  break;
12748
12886
  }
@@ -12755,32 +12893,59 @@ class WebSocketClient {
12755
12893
  break;
12756
12894
  }
12757
12895
  case "pong":
12896
+ this.clearPongTimeout();
12758
12897
  break;
12759
12898
  case "error": {
12760
12899
  const payload = message.payload;
12761
12900
  this.options.onError(new Error(`Server error: ${payload.message}`));
12762
12901
  break;
12763
12902
  }
12903
+ case "token_revoked":
12904
+ case "session_revoked": {
12905
+ this.shouldReconnect = false;
12906
+ this.options.onError(new Error("Your CLI token has been revoked. Please run `nbound login` again."));
12907
+ this.disconnect();
12908
+ break;
12909
+ }
12910
+ case "plan_changed": {
12911
+ const payload = message.payload;
12912
+ this.plan = payload.plan;
12913
+ break;
12914
+ }
12915
+ case "endpoint_deleted": {
12916
+ this.shouldReconnect = false;
12917
+ this.options.onError(new Error("This endpoint has been deleted."));
12918
+ this.disconnect();
12919
+ break;
12920
+ }
12921
+ default: {
12922
+ const unknownType2 = message.type;
12923
+ if (unknownType2) {}
12924
+ }
12764
12925
  }
12765
12926
  }
12766
12927
  sendAuth() {
12767
- this.send({
12928
+ if (!this.send({
12768
12929
  type: "auth",
12769
12930
  payload: { token: this.options.token }
12770
- });
12931
+ })) {
12932
+ this.options.onError(new Error("Failed to send auth message"));
12933
+ }
12771
12934
  }
12772
12935
  sendForwardSubscribe() {
12773
- this.send({
12936
+ if (!this.send({
12774
12937
  type: "forward_subscribe",
12775
12938
  payload: {
12776
12939
  endpointIds: [this.options.endpointId],
12777
12940
  port: this.options.port,
12778
12941
  host: this.options.host
12779
12942
  }
12780
- });
12943
+ })) {
12944
+ this.options.onError(new Error("Failed to send subscribe message"));
12945
+ }
12781
12946
  }
12782
12947
  sendForwardResponse(requestId, response) {
12783
- this.send({
12948
+ return this.send({
12784
12949
  type: "forward_response",
12785
12950
  payload: {
12786
12951
  requestId,
@@ -12794,7 +12959,7 @@ class WebSocketClient {
12794
12959
  });
12795
12960
  }
12796
12961
  sendForwardError(requestId, error) {
12797
- this.send({
12962
+ return this.send({
12798
12963
  type: "forward_error",
12799
12964
  payload: {
12800
12965
  requestId,
@@ -12807,17 +12972,42 @@ class WebSocketClient {
12807
12972
  }
12808
12973
  send(message) {
12809
12974
  if (this.ws && this.ws.readyState === wrapper_default.OPEN) {
12810
- this.ws.send(JSON.stringify(message));
12975
+ try {
12976
+ this.ws.send(JSON.stringify(message));
12977
+ return true;
12978
+ } catch {
12979
+ return false;
12980
+ }
12811
12981
  }
12982
+ return false;
12812
12983
  }
12813
12984
  startPingInterval() {
12814
12985
  this.pingInterval = setInterval(() => {
12815
- this.send({ type: "ping" });
12986
+ if (this.send({ type: "ping" })) {
12987
+ this.startPongTimeout();
12988
+ }
12816
12989
  }, PING_INTERVAL_MS);
12817
12990
  }
12991
+ startPongTimeout() {
12992
+ this.clearPongTimeout();
12993
+ this.pongTimeout = setTimeout(() => {
12994
+ if (this.state === "subscribed" && this.shouldReconnect) {
12995
+ this.options.onError(new Error("Connection timeout - no pong received"));
12996
+ this.cleanup();
12997
+ this.scheduleReconnect();
12998
+ this.options.onDisconnected("Ping timeout");
12999
+ }
13000
+ }, PONG_TIMEOUT_MS);
13001
+ }
13002
+ clearPongTimeout() {
13003
+ if (this.pongTimeout) {
13004
+ clearTimeout(this.pongTimeout);
13005
+ this.pongTimeout = null;
13006
+ }
13007
+ }
12818
13008
  startAuthTimeout() {
12819
13009
  this.authTimeout = setTimeout(() => {
12820
- if (!this.authenticated) {
13010
+ if (this.state === "authenticating") {
12821
13011
  this.shouldReconnect = false;
12822
13012
  this.options.onError(new Error("Authentication timed out"));
12823
13013
  this.disconnect();
@@ -12831,18 +13021,28 @@ class WebSocketClient {
12831
13021
  }
12832
13022
  }
12833
13023
  cleanup() {
12834
- this.isConnected = false;
13024
+ this.state = "disconnected";
12835
13025
  this.clearAuthTimeout();
13026
+ this.clearPongTimeout();
13027
+ this.clearReconnectTimeout();
12836
13028
  if (this.pingInterval) {
12837
13029
  clearInterval(this.pingInterval);
12838
13030
  this.pingInterval = null;
12839
13031
  }
12840
13032
  }
13033
+ clearReconnectTimeout() {
13034
+ if (this.reconnectTimeout) {
13035
+ clearTimeout(this.reconnectTimeout);
13036
+ this.reconnectTimeout = null;
13037
+ }
13038
+ }
12841
13039
  scheduleReconnect() {
13040
+ this.clearReconnectTimeout();
12842
13041
  this.reconnectAttempt++;
12843
13042
  const delay = getBackoffDelay(this.reconnectAttempt, 1000, 30000, true);
12844
13043
  this.options.onReconnecting(this.reconnectAttempt);
12845
- setTimeout(() => {
13044
+ this.reconnectTimeout = setTimeout(() => {
13045
+ this.reconnectTimeout = null;
12846
13046
  if (this.shouldReconnect) {
12847
13047
  this.doConnect();
12848
13048
  }
@@ -12858,7 +13058,7 @@ class WebSocketClient {
12858
13058
  }
12859
13059
  }
12860
13060
  get connected() {
12861
- return this.isConnected;
13061
+ return this.state === "subscribed";
12862
13062
  }
12863
13063
  }
12864
13064
 
@@ -12905,7 +13105,6 @@ class MultiEndpointClient {
12905
13105
  const url = new URL(baseUrl);
12906
13106
  url.searchParams.set("endpointId", endpointId);
12907
13107
  url.searchParams.set("type", "cli");
12908
- url.searchParams.set("token", this.options.token);
12909
13108
  return url.toString();
12910
13109
  }
12911
13110
  handleEndpointConnected(endpointId, subscribedIds, urls) {
@@ -12965,6 +13164,7 @@ class MultiEndpointClient {
12965
13164
  }
12966
13165
 
12967
13166
  // src/lib/forwarder.ts
13167
+ import { posix } from "node:path";
12968
13168
  var HOP_BY_HOP_HEADERS = new Set([
12969
13169
  "connection",
12970
13170
  "keep-alive",
@@ -12976,10 +13176,39 @@ var HOP_BY_HOP_HEADERS = new Set([
12976
13176
  "upgrade",
12977
13177
  "host"
12978
13178
  ]);
13179
+ var ALLOWED_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
13180
+ var MAX_RESPONSE_SIZE = 1024 * 1024;
13181
+ function normalizePath(pathPrefix, requestPath) {
13182
+ const prefix = pathPrefix.startsWith("/") ? pathPrefix : "/" + pathPrefix;
13183
+ const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
13184
+ const path2 = requestPath.startsWith("/") ? requestPath : "/" + requestPath;
13185
+ const combined = normalizedPrefix + path2;
13186
+ const normalized = posix.normalize(combined);
13187
+ const expectedRoot = normalizedPrefix || "/";
13188
+ if (!normalized.startsWith(expectedRoot) && normalized !== expectedRoot.slice(0, -1)) {
13189
+ if (!normalized.startsWith("/")) {
13190
+ return null;
13191
+ }
13192
+ }
13193
+ return normalized;
13194
+ }
12979
13195
  async function forwardRequest(request2, options) {
12980
13196
  const startTime = Date.now();
13197
+ if (!ALLOWED_HOSTS.has(options.host.toLowerCase())) {
13198
+ throw {
13199
+ code: "INVALID_HOST",
13200
+ message: `Host '${options.host}' is not allowed. Only localhost, 127.0.0.1, and ::1 are permitted.`
13201
+ };
13202
+ }
13203
+ const normalizedPath = normalizePath(options.pathPrefix, request2.path);
13204
+ if (normalizedPath === null) {
13205
+ throw {
13206
+ code: "INVALID_PATH",
13207
+ message: `Path traversal attempt detected in '${request2.path}'`
13208
+ };
13209
+ }
12981
13210
  const url = new URL(`http://${options.host}:${options.port}`);
12982
- url.pathname = options.pathPrefix + request2.path;
13211
+ url.pathname = normalizedPath;
12983
13212
  for (const [key, value] of Object.entries(request2.query)) {
12984
13213
  url.searchParams.set(key, value);
12985
13214
  }
@@ -12995,15 +13224,48 @@ async function forwardRequest(request2, options) {
12995
13224
  method: request2.method,
12996
13225
  headers,
12997
13226
  body: request2.body,
12998
- signal: AbortSignal.timeout(30000)
13227
+ signal: AbortSignal.timeout(30000),
13228
+ redirect: "manual"
12999
13229
  });
13230
+ const contentLength = response.headers.get("content-length");
13231
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
13232
+ throw {
13233
+ code: "RESPONSE_TOO_LARGE",
13234
+ message: `Response size ${contentLength} bytes exceeds limit of ${MAX_RESPONSE_SIZE} bytes`
13235
+ };
13236
+ }
13000
13237
  const responseHeaders = {};
13001
13238
  response.headers.forEach((value, key) => {
13002
13239
  if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
13003
13240
  responseHeaders[key] = value;
13004
13241
  }
13005
13242
  });
13006
- const body = await response.text();
13243
+ const reader = response.body?.getReader();
13244
+ if (!reader) {
13245
+ return {
13246
+ status: response.status,
13247
+ headers: responseHeaders,
13248
+ body: null,
13249
+ timeMs: Date.now() - startTime
13250
+ };
13251
+ }
13252
+ const chunks = [];
13253
+ let totalSize = 0;
13254
+ while (true) {
13255
+ const { done, value } = await reader.read();
13256
+ if (done)
13257
+ break;
13258
+ totalSize += value.length;
13259
+ if (totalSize > MAX_RESPONSE_SIZE) {
13260
+ reader.cancel();
13261
+ throw {
13262
+ code: "RESPONSE_TOO_LARGE",
13263
+ message: `Response size exceeds limit of ${MAX_RESPONSE_SIZE} bytes`
13264
+ };
13265
+ }
13266
+ chunks.push(value);
13267
+ }
13268
+ const body = new TextDecoder().decode(chunks.length === 1 ? chunks[0] : concatUint8Arrays(chunks));
13007
13269
  return {
13008
13270
  status: response.status,
13009
13271
  headers: responseHeaders,
@@ -13011,7 +13273,9 @@ async function forwardRequest(request2, options) {
13011
13273
  timeMs: Date.now() - startTime
13012
13274
  };
13013
13275
  } catch (error) {
13014
- const timeMs = Date.now() - startTime;
13276
+ if (error && typeof error === "object" && "code" in error) {
13277
+ throw error;
13278
+ }
13015
13279
  if (error instanceof Error) {
13016
13280
  if (error.cause && typeof error.cause === "object" && "code" in error.cause) {
13017
13281
  const code = error.cause.code;
@@ -13035,6 +13299,16 @@ async function forwardRequest(request2, options) {
13035
13299
  };
13036
13300
  }
13037
13301
  }
13302
+ function concatUint8Arrays(arrays) {
13303
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
13304
+ const result = new Uint8Array(totalLength);
13305
+ let offset = 0;
13306
+ for (const arr of arrays) {
13307
+ result.set(arr, offset);
13308
+ offset += arr.length;
13309
+ }
13310
+ return result;
13311
+ }
13038
13312
 
13039
13313
  // src/commands/connect.ts
13040
13314
  async function connectCommand(port, options) {
@@ -13252,6 +13526,57 @@ async function endpointsCommand(options) {
13252
13526
  }
13253
13527
  }
13254
13528
 
13529
+ // src/commands/create.ts
13530
+ async function createCommand2(nameArg, options) {
13531
+ const config = loadConfig();
13532
+ if (!config) {
13533
+ log.error("Not logged in");
13534
+ log.info("Run: nbound login");
13535
+ process.exit(2);
13536
+ }
13537
+ const name = nameArg || options.name;
13538
+ if (!name) {
13539
+ log.error("Endpoint name is required");
13540
+ log.info("Usage: nbound create <name>");
13541
+ log.info(" or: nbound create --name <name>");
13542
+ process.exit(4);
13543
+ }
13544
+ const spinner = createSpinner("Creating endpoint...");
13545
+ spinner.start();
13546
+ try {
13547
+ const endpoint = await createEndpoint(config.token, {
13548
+ name,
13549
+ description: options.description
13550
+ });
13551
+ spinner.succeed("Endpoint created");
13552
+ log.blank();
13553
+ log.bold(endpoint.name);
13554
+ log.blank();
13555
+ log.info(` URL: ${endpoint.url}`);
13556
+ log.info(` Slug: ${endpoint.slug}`);
13557
+ log.info(` ID: ${endpoint.id}`);
13558
+ log.blank();
13559
+ log.dim("Run 'nbound connect <port>' to start forwarding");
13560
+ log.blank();
13561
+ } catch (error) {
13562
+ if (error instanceof ApiError) {
13563
+ if (error.status === 401) {
13564
+ spinner.fail("Session expired");
13565
+ log.info("Run: nbound login");
13566
+ process.exit(2);
13567
+ }
13568
+ if (error.code === "PLAN_LIMIT") {
13569
+ spinner.fail("Endpoint limit reached");
13570
+ log.info("Upgrade at https://app.nbound.dev/settings/billing");
13571
+ process.exit(2);
13572
+ }
13573
+ spinner.fail(error.message);
13574
+ process.exit(1);
13575
+ }
13576
+ throw error;
13577
+ }
13578
+ }
13579
+
13255
13580
  // src/index.ts
13256
13581
  program.name("nbound").description("Forward webhooks to localhost for development").version("1.0.0");
13257
13582
  program.command("login").description("Authenticate with nbound").option("--token <token>", "Use an existing API token").action(loginCommand);
@@ -13259,7 +13584,8 @@ program.command("logout").description("Clear stored credentials").action(logoutC
13259
13584
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
13260
13585
  program.command("connect <port>").description("Forward webhooks to localhost").option("-e, --endpoint <id>", "Specific endpoint ID or slug").option("-p, --path <path>", "Path prefix for localhost").option("-h, --host <host>", "Local hostname", "localhost").option("--no-color", "Disable colored output").option("--json", "Output as JSON lines for scripting").option("-q, --quiet", "Minimal output").action(connectCommand);
13261
13586
  program.command("endpoints").description("List your endpoints").option("--json", "Output as JSON").action(endpointsCommand);
13587
+ program.command("create [name]").description("Create a new endpoint").option("-n, --name <name>", "Endpoint name").option("-d, --description <description>", "Endpoint description").action(createCommand2);
13262
13588
  program.parse();
13263
13589
 
13264
- //# debugId=1055C689E14F621164756E2164756E21
13590
+ //# debugId=4EAD0A99C960730564756E2164756E21
13265
13591
  //# sourceMappingURL=index.js.map