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/bin/nbound.js +374 -48
- package/dist/index.js +375 -49
- package/dist/index.js.map +12 -11
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
-
|
|
7009
|
-
|
|
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
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 &&
|
|
12844
|
+
if (this.shouldReconnect && prevState === "subscribed") {
|
|
12710
12845
|
this.scheduleReconnect();
|
|
12711
12846
|
this.options.onDisconnected(reasonStr);
|
|
12712
|
-
} else if (
|
|
12713
|
-
this.
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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=
|
|
13590
|
+
//# debugId=4EAD0A99C960730564756E2164756E21
|
|
13265
13591
|
//# sourceMappingURL=index.js.map
|