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.
- package/dist/bin/nbound.js +227 -92
- package/dist/index.js +227 -92
- package/dist/index.js.map +7 -9
- package/package.json +9 -4
package/dist/bin/nbound.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
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:
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
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
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
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">✓</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">✗</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
|
|
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
|
|
8587
|
-
|
|
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
|
-
|
|
8666
|
+
spinner.succeed(`Logged in as ${user.email}`);
|
|
8598
8667
|
} catch (error) {
|
|
8599
|
-
|
|
8668
|
+
spinner.fail("Invalid token");
|
|
8600
8669
|
if (error instanceof ApiError) {
|
|
8601
8670
|
log.error(error.message);
|
|
8602
8671
|
}
|
|
8603
|
-
process.exit(
|
|
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.
|
|
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,
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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.
|
|
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);
|