bhole 0.1.0-alpha.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/.turbo/turbo-build.log +4 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/http.d.ts +3 -0
- package/dist/commands/http.d.ts.map +1 -0
- package/dist/commands/http.js +95 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +24 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +40 -0
- package/dist/commands/tcp.d.ts +3 -0
- package/dist/commands/tcp.d.ts.map +1 -0
- package/dist/commands/tcp.js +10 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +38 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/lib/loginPlain.d.ts +2 -0
- package/dist/lib/loginPlain.d.ts.map +1 -0
- package/dist/lib/loginPlain.js +76 -0
- package/dist/lib/tunnelPlain.d.ts +2 -0
- package/dist/lib/tunnelPlain.d.ts.map +1 -0
- package/dist/lib/tunnelPlain.js +31 -0
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +44 -0
- package/dist/lib/words.d.ts +2 -0
- package/dist/lib/words.d.ts.map +1 -0
- package/dist/lib/words.js +24 -0
- package/dist/tunnel/http.d.ts +23 -0
- package/dist/tunnel/http.d.ts.map +1 -0
- package/dist/tunnel/http.js +267 -0
- package/dist/ui/LoginApp.d.ts +8 -0
- package/dist/ui/LoginApp.d.ts.map +1 -0
- package/dist/ui/LoginApp.js +129 -0
- package/dist/ui/TunnelApp.d.ts +12 -0
- package/dist/ui/TunnelApp.d.ts.map +1 -0
- package/dist/ui/TunnelApp.js +122 -0
- package/package.json +38 -0
- package/src/commands/config.ts +37 -0
- package/src/commands/http.tsx +117 -0
- package/src/commands/tcp.ts +11 -0
- package/src/config.ts +41 -0
- package/src/index.ts +20 -0
- package/src/lib/tunnelPlain.ts +45 -0
- package/src/lib/version.ts +45 -0
- package/src/lib/words.ts +25 -0
- package/src/tunnel/http.ts +315 -0
- package/src/ui/TunnelApp.tsx +252 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,eAAO,MAAM,aAAa,SAiCvB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loadConfig, saveConfig, getConfigDir } from "../config.js";
|
|
3
|
+
export const configCommand = new Command("config")
|
|
4
|
+
.description("Manage CLI configuration")
|
|
5
|
+
.addCommand(new Command("path")
|
|
6
|
+
.description("Show config file path")
|
|
7
|
+
.action(() => {
|
|
8
|
+
console.log(getConfigDir());
|
|
9
|
+
}))
|
|
10
|
+
.addCommand(new Command("list")
|
|
11
|
+
.description("Show current config (key hidden)")
|
|
12
|
+
.action(() => {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
console.log("Config location:", getConfigDir());
|
|
15
|
+
console.log("Tunnel domain:", config.tunnelDomain ?? "(not set — use config set-tunnel-domain)");
|
|
16
|
+
}))
|
|
17
|
+
.addCommand(new Command("set-tunnel-domain")
|
|
18
|
+
.description("Set the public domain for tunnel URLs (e.g. tunnel.yourdomain.com)")
|
|
19
|
+
.argument("<domain>", "Domain for tunnels (e.g. bhole.link)")
|
|
20
|
+
.action((domain) => {
|
|
21
|
+
const trimmed = domain.trim().toLowerCase().replace(/^https?:\/\//, "").split("/")[0];
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
console.error("Invalid domain");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
config.tunnelDomain = trimmed;
|
|
28
|
+
saveConfig(config);
|
|
29
|
+
console.log(`Tunnel domain set to ${trimmed}. Tunnels will be at https://{endpoint}.${trimmed}`);
|
|
30
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/commands/http.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwDpC,eAAO,MAAM,WAAW,SA2DpB,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import net from "net";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { runTunnelPlain } from "../lib/tunnelPlain.js";
|
|
6
|
+
import { randomMnemonicId } from "../lib/words.js";
|
|
7
|
+
const authToken = process.env.BHOLE_AUTH_TOKEN;
|
|
8
|
+
function isPortListening(port) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const socket = new net.Socket();
|
|
11
|
+
const timer = setTimeout(() => {
|
|
12
|
+
socket.destroy();
|
|
13
|
+
resolve(false);
|
|
14
|
+
}, 500);
|
|
15
|
+
socket.on("connect", () => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
socket.destroy();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
socket.on("error", () => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolve(false);
|
|
23
|
+
});
|
|
24
|
+
socket.connect(port, "127.0.0.1");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function runTunnel(serverUrl, endpoint, localPort, publicUrl, tunnelDomain) {
|
|
28
|
+
if (process.stdin.isTTY) {
|
|
29
|
+
const { render } = await import("ink");
|
|
30
|
+
const { TunnelApp } = await import("../ui/TunnelApp.js");
|
|
31
|
+
const { waitUntilExit } = render(_jsx(TunnelApp, { serverUrl: serverUrl, endpoint: endpoint, localPort: localPort, publicUrl: publicUrl, tunnelDomain: tunnelDomain, authToken: authToken, onExit: (code) => process.exit(code) }));
|
|
32
|
+
await waitUntilExit();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await runTunnelPlain(serverUrl, endpoint, localPort, publicUrl, authToken);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export const httpCommand = new Command("http")
|
|
39
|
+
.description("Expose a local HTTP server to the internet")
|
|
40
|
+
.argument("<port>", "Local port to expose")
|
|
41
|
+
.option("-s, --server <url>", "Tunnel server URL (default: ws://localhost:8080)", "ws://localhost:8080")
|
|
42
|
+
.option("-d, --domain <domain>", "Static subdomain (e.g. --domain=myapp)")
|
|
43
|
+
.option("--subdomain <name>", "Custom subdomain (alias for --domain)")
|
|
44
|
+
.action(async (portStr, options) => {
|
|
45
|
+
const port = parseInt(portStr, 10);
|
|
46
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
47
|
+
console.error("Error: port must be a valid number between 1 and 65535");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
const tunnelDomain = config.tunnelDomain ?? process.env.BHOLE_TUNNEL_DOMAIN;
|
|
52
|
+
let serverUrl = (options.server || process.env.BHOLE_SERVER_URL || "ws://localhost:8080").replace(/^http/, "ws");
|
|
53
|
+
if (!serverUrl.startsWith("ws"))
|
|
54
|
+
serverUrl = "ws://" + serverUrl;
|
|
55
|
+
if (!tunnelDomain && !serverUrl.includes("localhost")) {
|
|
56
|
+
console.error("Error: Set tunnel domain with 'bhole config set-tunnel-domain <domain>' or BHOLE_TUNNEL_DOMAIN");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const effectiveDomain = tunnelDomain ?? "localhost";
|
|
60
|
+
const domainOrSub = options.domain ?? options.subdomain;
|
|
61
|
+
let endpoint;
|
|
62
|
+
let publicUrl;
|
|
63
|
+
if (domainOrSub) {
|
|
64
|
+
let hostname = domainOrSub.trim();
|
|
65
|
+
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
|
66
|
+
try {
|
|
67
|
+
hostname = new URL(hostname).hostname;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
hostname = domainOrSub;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (hostname.includes(".")) {
|
|
74
|
+
endpoint = hostname.split(".")[0] ?? hostname;
|
|
75
|
+
publicUrl = domainOrSub.startsWith("http") ? domainOrSub : `https://${hostname}`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
endpoint = hostname;
|
|
79
|
+
publicUrl = `https://${endpoint}.${effectiveDomain}`;
|
|
80
|
+
}
|
|
81
|
+
if (!endpoint || endpoint.length > 63) {
|
|
82
|
+
console.error("Error: endpoint must be 1–63 characters");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
endpoint = randomMnemonicId();
|
|
88
|
+
publicUrl = `https://${endpoint}.${effectiveDomain}`;
|
|
89
|
+
}
|
|
90
|
+
const portOk = await isPortListening(port);
|
|
91
|
+
if (!portOk) {
|
|
92
|
+
console.warn(`Note: Nothing appears to be listening on localhost:${port}. Requests will return 502 until your server is running.`);
|
|
93
|
+
}
|
|
94
|
+
await runTunnel(serverUrl, endpoint, port, publicUrl, effectiveDomain);
|
|
95
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyBpC,eAAO,MAAM,YAAY,SAUrB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { runLoginPlain } from "../lib/loginPlain.js";
|
|
4
|
+
const DEFAULT_SITE_URL = process.env.BHOLE_SITE_URL ?? "http://localhost:2465";
|
|
5
|
+
async function runLogin(siteUrl, apiKey) {
|
|
6
|
+
if (process.stdin.isTTY) {
|
|
7
|
+
const { render } = await import("ink");
|
|
8
|
+
const { LoginApp } = await import("../ui/LoginApp.js");
|
|
9
|
+
const { waitUntilExit } = render(_jsx(LoginApp, { siteUrl: siteUrl, apiKey: apiKey, onExit: (code) => process.exit(code) }));
|
|
10
|
+
await waitUntilExit();
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const code = await runLoginPlain(siteUrl, apiKey);
|
|
14
|
+
process.exit(code);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const loginCommand = new Command("login")
|
|
18
|
+
.description("Sign in and store your API key for tunneling")
|
|
19
|
+
.option("-u, --url <url>", "Dashboard URL to open", DEFAULT_SITE_URL)
|
|
20
|
+
.argument("[apiKey]", "API key from the dashboard (optional - will open browser if not provided)")
|
|
21
|
+
.action(async (apiKey, options) => {
|
|
22
|
+
const siteUrl = options?.url ?? process.env.BHOLE_SITE_URL ?? DEFAULT_SITE_URL;
|
|
23
|
+
await runLogin(siteUrl, apiKey);
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,aAAa,SAoCtB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loadConfig, saveConfig } from "../config.js";
|
|
3
|
+
const DEFAULT_SITE_URL = process.env.BHOLE_SITE_URL ?? "http://localhost:2465";
|
|
4
|
+
export const logoutCommand = new Command("logout")
|
|
5
|
+
.description("Sign out and clear stored API key")
|
|
6
|
+
.option("-u, --url <url>", "Dashboard URL", DEFAULT_SITE_URL)
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
if (!config.apiKey) {
|
|
10
|
+
console.log("You are not signed in.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const siteUrl = options?.url ?? config.siteUrl ?? DEFAULT_SITE_URL;
|
|
14
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
15
|
+
const apiKey = config.apiKey;
|
|
16
|
+
delete config.apiKey;
|
|
17
|
+
saveConfig(config);
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${baseUrl}/api/cli-auth/revoke`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({ key: apiKey }),
|
|
23
|
+
});
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
const data = (await res.json());
|
|
26
|
+
if (data.revoked) {
|
|
27
|
+
console.log("Signed out and API key revoked.");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log("Signed out successfully.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log("Signed out successfully.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.log("Signed out successfully.");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tcp.d.ts","sourceRoot":"","sources":["../../src/commands/tcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,eAAO,MAAM,UAAU,SAQnB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export const tcpCommand = new Command("tcp")
|
|
3
|
+
.description("Expose a local TCP port to the internet (coming soon)")
|
|
4
|
+
.argument("<port>", "Local port to expose")
|
|
5
|
+
.option("-s, --server <url>", "Blackhole server URL", "ws://localhost:8081")
|
|
6
|
+
.option("--subdomain <name>", "Custom subdomain for the tunnel")
|
|
7
|
+
.action((_portStr) => {
|
|
8
|
+
console.log("TCP tunneling is not yet implemented. Use 'bhole http <port>' for HTTP.");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,aAAa,SAoCtB,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
const DEFAULT_SITE_URL = process.env.BHOLE_SITE_URL ?? "http://localhost:2465";
|
|
4
|
+
export const whoamiCommand = new Command("whoami")
|
|
5
|
+
.description("Show current signed-in identity")
|
|
6
|
+
.option("-u, --url <url>", "Dashboard URL", DEFAULT_SITE_URL)
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
if (!config.apiKey) {
|
|
10
|
+
console.log("You are not signed in.");
|
|
11
|
+
console.log("Run bhole login to sign in.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const siteUrl = options?.url ?? config.siteUrl ?? DEFAULT_SITE_URL;
|
|
15
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
16
|
+
console.log("Signed in");
|
|
17
|
+
console.log("API key:", "***" + config.apiKey.slice(-4));
|
|
18
|
+
if (config.siteUrl) {
|
|
19
|
+
console.log("Dashboard:", config.siteUrl);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${baseUrl}/api/whoami`, {
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (res.ok) {
|
|
28
|
+
const user = (await res.json());
|
|
29
|
+
const display = user.name || user.email;
|
|
30
|
+
if (display) {
|
|
31
|
+
console.log("User:", display);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Silently ignore - we already showed key/site
|
|
37
|
+
}
|
|
38
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
/** Public domain for tunnel URLs. Tunnels are reachable at https://{endpoint}.{tunnelDomain} */
|
|
3
|
+
tunnelDomain?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getConfigDir(): string;
|
|
6
|
+
export declare function loadConfig(): Config;
|
|
7
|
+
export declare function saveConfig(config: Config): void;
|
|
8
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,MAAM;IACrB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAOnC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAe/C"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CONFIG_DIR = path.join(homedir(), ".blackhole");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function getConfigDir() {
|
|
7
|
+
return CONFIG_DIR;
|
|
8
|
+
}
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
12
|
+
return JSON.parse(data);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function saveConfig(config) {
|
|
19
|
+
try {
|
|
20
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
21
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
22
|
+
}
|
|
23
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
24
|
+
try {
|
|
25
|
+
fs.chmodSync(CONFIG_DIR, 0o700);
|
|
26
|
+
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// chmod may no-op on some platforms (e.g. Windows); ignore
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
throw new Error(`Failed to save config: ${err}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { configCommand } from "./commands/config.js";
|
|
4
|
+
import { httpCommand } from "./commands/http.js";
|
|
5
|
+
import { tcpCommand } from "./commands/tcp.js";
|
|
6
|
+
import { CLI_VERSION } from "./lib/version.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("bhole")
|
|
10
|
+
.description("Expose local services to the internet - tunnel localhost with one command")
|
|
11
|
+
.version(CLI_VERSION);
|
|
12
|
+
program.addCommand(configCommand);
|
|
13
|
+
program.addCommand(httpCommand);
|
|
14
|
+
program.addCommand(tcpCommand);
|
|
15
|
+
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loginPlain.d.ts","sourceRoot":"","sources":["../../src/lib/loginPlain.ts"],"names":[],"mappings":"AAKA,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CA+EjB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { loadConfig, saveConfig } from "../config.js";
|
|
2
|
+
const POLL_INTERVAL_MS = 1500;
|
|
3
|
+
const MAX_WAIT_MS = 10 * 60 * 1000;
|
|
4
|
+
export async function runLoginPlain(siteUrl, apiKey) {
|
|
5
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
if (!apiKey?.trim() && config.apiKey) {
|
|
8
|
+
console.log("You are already signed in.");
|
|
9
|
+
console.log("API key: ***" + config.apiKey.slice(-4));
|
|
10
|
+
console.log("Run bhole logout to sign out.");
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
if (apiKey?.trim()) {
|
|
14
|
+
const key = apiKey.trim();
|
|
15
|
+
if (!key.startsWith("bh_")) {
|
|
16
|
+
console.error("Invalid API key format. Keys should start with bh_");
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
config.apiKey = key;
|
|
20
|
+
config.siteUrl = siteUrl;
|
|
21
|
+
saveConfig(config);
|
|
22
|
+
console.log("API key saved successfully.");
|
|
23
|
+
console.log("You can now run: bhole http 3000");
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const startRes = await fetch(`${baseUrl}/api/cli-auth/start`, { method: "POST" });
|
|
28
|
+
if (!startRes.ok) {
|
|
29
|
+
console.error("Failed to start login. Is the dashboard running?");
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const { code } = (await startRes.json());
|
|
33
|
+
if (!code) {
|
|
34
|
+
console.error("Invalid response from dashboard.");
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
const cliAuthUrl = `${baseUrl}/cli-auth?code=${code}`;
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(" Open this URL to sign in:");
|
|
40
|
+
console.log(` ${cliAuthUrl}`);
|
|
41
|
+
console.log("");
|
|
42
|
+
try {
|
|
43
|
+
const { default: open } = await import("open");
|
|
44
|
+
await open(cliAuthUrl);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
console.log(" (Could not open browser automatically)");
|
|
48
|
+
}
|
|
49
|
+
console.log(" Waiting for you to complete sign-in in the browser...");
|
|
50
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
53
|
+
const statusRes = await fetch(`${baseUrl}/api/cli-auth/status?code=${encodeURIComponent(code)}`);
|
|
54
|
+
const data = (await statusRes.json());
|
|
55
|
+
if (data.status === "ready" && data.apiKey) {
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
config.apiKey = data.apiKey;
|
|
58
|
+
config.siteUrl = siteUrl;
|
|
59
|
+
saveConfig(config);
|
|
60
|
+
console.log("API key saved successfully.");
|
|
61
|
+
console.log("You can now run: bhole http 3000");
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (data.status === "expired" || data.status === "invalid") {
|
|
65
|
+
console.error("Login expired or invalid. Run bhole login again.");
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.error("Login timed out. Run bhole login again.");
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.error(e instanceof Error ? e.message : "Login failed.");
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnelPlain.d.ts","sourceRoot":"","sources":["../../src/lib/tunnelPlain.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHttpTunnel } from "../tunnel/http.js";
|
|
2
|
+
const MAX_RETRIES = 2;
|
|
3
|
+
const BASE_DELAY_MS = 2000;
|
|
4
|
+
export async function runTunnelPlain(serverUrl, endpoint, localPort, publicUrl, authToken) {
|
|
5
|
+
const wsUrl = serverUrl.replace(/^http/, "ws");
|
|
6
|
+
console.log(`Connecting to ${wsUrl}...`);
|
|
7
|
+
console.log(`Exposing localhost:${localPort} at ${publicUrl}`);
|
|
8
|
+
const tryConnect = (attempt) => new Promise((resolve, reject) => {
|
|
9
|
+
createHttpTunnel({ serverUrl: wsUrl, endpoint, localPort, authToken }, {
|
|
10
|
+
onReady: () => {
|
|
11
|
+
console.log("Tunnel established. Forwarding traffic to localhost:" + localPort);
|
|
12
|
+
console.log(`URL: ${publicUrl}`);
|
|
13
|
+
console.log(`(or use X-Blackhole-Endpoint: ${endpoint})`);
|
|
14
|
+
resolve();
|
|
15
|
+
},
|
|
16
|
+
onError: (err) => {
|
|
17
|
+
console.error("Tunnel error:", err.message);
|
|
18
|
+
},
|
|
19
|
+
}).catch((err) => {
|
|
20
|
+
if (attempt < MAX_RETRIES) {
|
|
21
|
+
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
22
|
+
console.error(`Connection failed. Retrying in ${delayMs / 1000}s...`);
|
|
23
|
+
setTimeout(() => tryConnect(attempt + 1).then(resolve, reject), delayMs);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
reject(err);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
tryConnect(0).catch(() => process.exit(1));
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,QAAc,CAAC;AAoBvC,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAW/D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAGlE"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require(path.join(__dirname, "../../package.json"));
|
|
7
|
+
export const CLI_VERSION = pkg.version;
|
|
8
|
+
/** Compare semver strings. Returns true if a > b. */
|
|
9
|
+
function isNewer(a, b) {
|
|
10
|
+
const parse = (s) => s
|
|
11
|
+
.replace(/[-].*$/, "")
|
|
12
|
+
.split(".")
|
|
13
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
14
|
+
const pa = parse(a);
|
|
15
|
+
const pb = parse(b);
|
|
16
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
17
|
+
const va = pa[i] ?? 0;
|
|
18
|
+
const vb = pb[i] ?? 0;
|
|
19
|
+
if (va > vb)
|
|
20
|
+
return true;
|
|
21
|
+
if (va < vb)
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export async function getLatestVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch("https://registry.npmjs.org/bhole/latest", {
|
|
29
|
+
signal: AbortSignal.timeout(3000),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
return null;
|
|
33
|
+
const data = (await res.json());
|
|
34
|
+
return data.version ?? null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function shouldSuggestUpdate(latest) {
|
|
41
|
+
if (!latest)
|
|
42
|
+
return false;
|
|
43
|
+
return isNewer(latest, CLI_VERSION);
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"words.d.ts","sourceRoot":"","sources":["../../src/lib/words.ts"],"names":[],"mappings":"AAkBA,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mnemonic word list for ngrok-style random subdomains (e.g. happy-blue-frog).
|
|
3
|
+
* Short, memorable, URL-safe words.
|
|
4
|
+
*/
|
|
5
|
+
const WORDS = [
|
|
6
|
+
"angel", "apple", "arrow", "beach", "bear", "bird", "blue", "boat", "bold",
|
|
7
|
+
"bone", "book", "boss", "bush", "cake", "cash", "cat", "cave", "cold",
|
|
8
|
+
"cord", "crab", "cube", "dark", "demo", "dice", "dock", "door", "dove",
|
|
9
|
+
"dusk", "edge", "echo", "elm", "fall", "fern", "fire", "fish", "flag",
|
|
10
|
+
"flat", "fog", "fork", "frog", "gold", "gray", "grey", "grid", "gulf",
|
|
11
|
+
"harp", "hill", "hope", "iris", "iron", "java", "jazz", "key", "kite",
|
|
12
|
+
"lake", "lamp", "lane", "leaf", "lime", "lion", "log", "map", "mask",
|
|
13
|
+
"mint", "mist", "moon", "nova", "oak", "ocean", "olive", "opal", "pine",
|
|
14
|
+
"pink", "pool", "port", "rain", "reed", "rock", "rose", "rust", "sand",
|
|
15
|
+
"seed", "sky", "snow", "star", "sun", "surf", "swan", "tide", "tree",
|
|
16
|
+
"vine", "wave", "wolf", "wood", "zen",
|
|
17
|
+
];
|
|
18
|
+
export function randomMnemonicId() {
|
|
19
|
+
const picks = [];
|
|
20
|
+
for (let i = 0; i < 3; i++) {
|
|
21
|
+
picks.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
|
|
22
|
+
}
|
|
23
|
+
return picks.join("-");
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface HttpTunnelOptions {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
localPort: number;
|
|
5
|
+
/** Shared secret for auth (from BHOLE_AUTH_TOKEN) */
|
|
6
|
+
authToken?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TunnelRequestInfo {
|
|
9
|
+
method: string;
|
|
10
|
+
path: string;
|
|
11
|
+
bytesIn: number;
|
|
12
|
+
bytesOut: number;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
}
|
|
16
|
+
export interface HttpTunnelCallbacks {
|
|
17
|
+
onOpen?: () => void;
|
|
18
|
+
onReady?: () => void;
|
|
19
|
+
onRequest?: (info: TunnelRequestInfo) => void;
|
|
20
|
+
onError?: (err: Error) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createHttpTunnel(options: HttpTunnelOptions, callbacks?: HttpTunnelCallbacks): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/tunnel/http.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC9C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AAQD,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,SAAS,CAAC,EAAE,mBAAmB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAgJf"}
|