locadot 1.6.0-beta.4 → 1.6.0-beta.6

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/CHANGELOG.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- The version in `package.json` is 1.6.0-beta.4 (published under the `beta` tag). The owner decides the bump. **2.0.0** is suggested because of the
5
+ The version in `package.json` is 1.6.0-beta.6 (published under the `beta` tag). The owner decides the bump. **2.0.0** is suggested because of the
6
6
  breaking items below.
7
7
 
8
+ ### Changed
9
+ - Internal restructure into `cli/commands/*`, `server/*` and `proxy/*` modules. There are no behaviour changes; see `tasks/README.md`.
10
+
8
11
  ### Breaking
9
12
  - The proxy binds to `127.0.0.1` and `::1` only. Set `LOCADOT_BIND=0.0.0.0` for the old LAN-exposed behaviour.
10
13
  - The registry format is now v2 (`{version, hosts:{host:{target,…}}}`). The old `{host: port}` files are migrated
@@ -13,6 +16,8 @@ breaking items below.
13
16
  - Bare `localhost` is reserved for the dashboard and can't be mapped.
14
17
 
15
18
  ### Added
19
+ - `locadot tunnel --host <h>` (with `--off`, and `tunnel:install`) shares a mapping on a public `https://*.trycloudflare.com` URL through a Cloudflare quick tunnel, with no account needed. `cloudflared` is downloaded on demand into the state dir. The dashboard has a Cloudflare Tunnel card, a Public URL column and Share/Unshare per row. The API accepts `PUT /api/hosts/:host {"tunnel": true}` and has `POST /api/cloudflared/install`. `--cors` also works for tunnel visitors (the shim, preflights and origin rewriting to the public URL), but their pass-through calls may only reach public addresses; loopback, LAN and metadata addresses are refused at connect time.
20
+ - The dashboard is redesigned: a neutral palette that follows the system light/dark theme, a responsive grid and a single Traffic column.
16
21
  - `locadot start --port <http> --https-port <https>` (also on `restart`) runs the proxy on other ports than 80/443. The choice is saved in the state dir, so `add`, `status`, `open` and start-at-boot keep using it; `--port 80 --https-port 443` goes back. Running it while the proxy is up on other ports restarts it on the new ones.
17
22
  - `--cors` per mapping (CLI, API and dashboard). `Origin`/`Referer` are sent as the target's own origin, CORS preflights are answered locally, and any origin may call the domain with credentials. Absolute URLs of other mapped domains in text responses are rewritten to their `.localhost` names, and a mapped caller is sent as its real origin, so a site plus its API both work locally. Pages also get a small script that sends calls to any other origin through the page's own origin (`/__locadot/x/…`), so no mapping per API or third-party domain is needed. (ENH-12, BUG-15)
18
23
  - A control panel at `https://localhost` with a dark theme. You can add, edit and remove mappings, toggle CA trust and start-at-boot, see root/admin and privileged-port status,
package/README.md CHANGED
@@ -69,6 +69,23 @@ Options for `add` / `update`:
69
69
  | `locadot status [--json]` | Is it running, its PID, ports, number of hosts, CA trust, start-at-boot, dashboard URL. |
70
70
  | `locadot doctor [--host h]` | Check the proxy, ports, permissions, CA trust, the registry and every target. Exits 1 if anything fails. |
71
71
 
72
+ ### Sharing (Cloudflare Tunnel)
73
+
74
+ | Command | What it does |
75
+ | --- | --- |
76
+ | `locadot tunnel --host app.localhost` | Share a mapping on a public `https://<random>.trycloudflare.com` URL through a Cloudflare quick tunnel. No Cloudflare account is needed. `cloudflared` is downloaded on first use. |
77
+ | `locadot tunnel --host app.localhost --off` | Stop sharing it. |
78
+ | `locadot tunnel` | List the shared mappings and their public URLs. |
79
+ | `locadot tunnel:install` | Download `cloudflared` into the state dir (`<state>/bin`). Set `LOCADOT_CLOUDFLARED` to use your own binary; one on `PATH` is also picked up. |
80
+
81
+ The tunnel runs inside the proxy, so a shared mapping keeps working after `restart` and comes back at boot. Redirects to the
82
+ target are rewritten to the public URL. **Anyone with the link can reach the mapping**, so only share what you'd put online.
83
+ `--cors` works through the tunnel too: CORS headers and preflights, the pass-through shim, and the target's own
84
+ origin in pages rewritten to the public URL (other shared mappings get their public URLs too). For safety, a tunnel visitor's
85
+ pass-through calls may only go to public addresses: loopback, LAN, link-local and cloud-metadata addresses are refused, checked
86
+ on the address actually dialled, so the tunnel can't be used to reach your machine or network. The dashboard can also do this:
87
+ use **Share** / **Unshare** on a row.
88
+
72
89
  ### Certificates
73
90
 
74
91
  | Command | What it does |
@@ -91,12 +108,13 @@ Options for `add` / `update`:
91
108
  ## 📋 Dashboard & control panel
92
109
 
93
110
  Open `https://localhost` or `http://localhost` (or run `locadot open`). Requests for bare `localhost` / `127.0.0.1` / `::1`
94
- are answered by locadot itself, not proxied. The page is a dark control panel:
111
+ are answered by locadot itself, not proxied. The page is a control panel that follows the system light/dark theme:
95
112
 
96
113
  - **System:** whether the proxy runs as root/admin, whether ports below 1024 can be bound (with the Linux sysctl fix when they
97
114
  can't), CA trust with an on/off toggle, start-at-boot with an on/off toggle, ports, bind addresses and state dir. It also has a **Stop proxy** button.
98
115
  - **Proxies:** add a mapping (host + port / host:port / URL, and optional "Insecure TLS" and "Bypass CORS" boxes), edit a target inline, or remove one. The table shows
99
- each source → destination with up/down, status, latency, hits, errors, last access and average ms. It refreshes every 5 s.
116
+ each source → destination with up/down, status, latency, its public tunnel URL and traffic (hits, errors, average ms). It refreshes every 5 s.
117
+ - **Cloudflare Tunnel:** whether `cloudflared` is installed (with an **Install** button), and a **Share** button per row that puts the mapping on a public URL.
100
118
  - **Logs:** a live tail with refresh and clear.
101
119
  - **CLI / API snippets** you can copy.
102
120
 
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.doctor = doctor;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const localhost_1 = __importDefault(require("../../lib/localhost"));
10
+ const proxy_control_1 = __importDefault(require("../../lib/proxy-control"));
11
+ const registry_1 = __importDefault(require("../../lib/registry"));
12
+ const constants_1 = __importDefault(require("../../constants"));
13
+ const trust_1 = require("../../utils/trust");
14
+ const shared_1 = require("../shared");
15
+ async function doctor(options) {
16
+ let failures = 0;
17
+ const check = (ok, label, hint) => {
18
+ if (ok === undefined)
19
+ (0, shared_1.print)(`➖ ${label}`);
20
+ else
21
+ (0, shared_1.print)(`${ok ? "✅" : "❌"} ${label}${!ok && hint ? `\n → ${hint}` : ""}`);
22
+ if (ok === false)
23
+ failures++;
24
+ };
25
+ const info = proxy_control_1.default.running();
26
+ check(Boolean(info), `Proxy process ${info ? `running (pid ${info.pid})` : "not running"}`, "Run `locadot start` and read the error it prints.");
27
+ for (const port of [constants_1.default.server.httpPort, constants_1.default.server.httpsPort]) {
28
+ const listening = await localhost_1.default.isPortListening(port);
29
+ if (info) {
30
+ check(listening, `Port ${port} answering`, "The proxy is running but not listening; check `locadot logs`.");
31
+ }
32
+ else if (listening) {
33
+ check(false, `Port ${port} is free`, `Something else holds port ${port}. Find it with \`sudo lsof -i :${port}\`, or pick another with \`locadot start ${port === constants_1.default.server.httpPort ? "--port" : "--https-port"} <port>\`.`);
34
+ }
35
+ }
36
+ if (info && (await localhost_1.default.isPortListening(constants_1.default.server.httpPort))) {
37
+ const probe = await localhost_1.default.probe(`http://127.0.0.1:${constants_1.default.server.httpPort}/healthz`);
38
+ check(probe.up && probe.status === 200, "Dashboard responds", "Another program may be answering on the proxy port.");
39
+ }
40
+ if (os_1.default.platform() === "linux" && Math.min(constants_1.default.server.httpPort, constants_1.default.server.httpsPort) < 1024) {
41
+ let unprivileged = 1024;
42
+ try {
43
+ unprivileged = Number(fs_1.default.readFileSync("/proc/sys/net/ipv4/ip_unprivileged_port_start", "utf8"));
44
+ }
45
+ catch { }
46
+ const canBind = process.getuid?.() === 0 || unprivileged <= Math.min(constants_1.default.server.httpPort, constants_1.default.server.httpsPort);
47
+ check(canBind || Boolean(info), "Allowed to bind ports 80/443", "Run with sudo, or allow it once: `sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80` (persist in /etc/sysctl.d/).");
48
+ }
49
+ const trusted = await (0, trust_1.isCATrusted)().catch(() => false);
50
+ check(trusted, "Locadot CA trusted by the system", `Run \`locadot trust\`. Manual steps:\n${(0, trust_1.trustInstructions)()}`);
51
+ try {
52
+ const registry = registry_1.default.read();
53
+ check(true, `Registry readable (${Object.keys(registry.hosts).length} host(s))`);
54
+ const hosts = options.host ? [localhost_1.default.requireHost(options.host)] : Object.keys(registry.hosts);
55
+ for (const host of hosts) {
56
+ const entry = registry.hosts[host];
57
+ if (!entry) {
58
+ check(false, `${host} is mapped`, `locadot add --host ${host} --port <port>`);
59
+ continue;
60
+ }
61
+ const probe = await localhost_1.default.probe(entry.target, 2000, entry.insecure);
62
+ check(probe.up, `${host} → ${entry.target} ${probe.up ? `answers (${probe.status}, ${probe.ms}ms)` : `unreachable (${probe.error})`}`, "Start the app, or fix the target with `locadot update`.");
63
+ }
64
+ }
65
+ catch (error) {
66
+ check(false, "Registry readable", error.message);
67
+ }
68
+ (0, shared_1.print)(failures ? `\n${failures} problem(s) found.` : "\nAll checks passed.");
69
+ if (failures)
70
+ process.exitCode = 1;
71
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.logs = logs;
40
+ exports.clearLogs = clearLogs;
41
+ exports.logPath = logPath;
42
+ exports.hostPath = hostPath;
43
+ exports.configPath = configPath;
44
+ exports.token = token;
45
+ const localhost_1 = __importStar(require("../../lib/localhost"));
46
+ const locadot_file_1 = __importDefault(require("../../lib/locadot-file"));
47
+ const proxy_control_1 = __importDefault(require("../../lib/proxy-control"));
48
+ const constants_1 = __importDefault(require("../../constants"));
49
+ const file_1 = __importDefault(require("../../utils/file"));
50
+ const certs_1 = require("../../utils/certs");
51
+ const shared_1 = require("../shared");
52
+ function logs(options) {
53
+ const lines = options.lines ? localhost_1.default.parsePort(options.lines) : 50;
54
+ if (options.follow === false) {
55
+ console.log(file_1.default.lastLines("LOGS", lines).join("\n") || "(log is empty)");
56
+ return;
57
+ }
58
+ locadot_file_1.default.watchLogs(lines);
59
+ }
60
+ function clearLogs() {
61
+ locadot_file_1.default.clearLogs();
62
+ (0, shared_1.print)("☑️ Logs successfully cleared.");
63
+ }
64
+ function logPath() {
65
+ console.log(constants_1.default.paths.LOGS);
66
+ }
67
+ function hostPath() {
68
+ console.log(constants_1.default.paths.REGISTRY_FILE);
69
+ }
70
+ function configPath() {
71
+ const rows = [
72
+ ["State dir", constants_1.default.paths.HOME],
73
+ ["Registry", constants_1.default.paths.REGISTRY_FILE],
74
+ ["Lock file", constants_1.default.paths.LOCK_FILE],
75
+ ["Logs", constants_1.default.paths.LOGS],
76
+ ["Certs", constants_1.default.paths.CERT_DIR],
77
+ ["CA cert", (0, certs_1.caCertPath)()],
78
+ ["API token", constants_1.default.paths.API_TOKEN],
79
+ ["Config", constants_1.default.paths.CONFIG_FILE],
80
+ ];
81
+ rows.forEach(([label, value]) => console.log(`${label.padEnd(10)} ${value}`));
82
+ }
83
+ /** For scripts and AI agents driving the dashboard API. */
84
+ function token() {
85
+ const token = proxy_control_1.default.running() ? file_1.default.read("API_TOKEN")?.trim() : undefined;
86
+ if (!token)
87
+ throw new localhost_1.InputError("❌ The proxy isn't running, so there's no API token. Run `locadot start`.");
88
+ console.log(token);
89
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.add = add;
40
+ exports.update = update;
41
+ exports.remove = remove;
42
+ exports.list = list;
43
+ exports.clearHosts = clearHosts;
44
+ const localhost_1 = __importStar(require("../../lib/localhost"));
45
+ const registry_1 = __importDefault(require("../../lib/registry"));
46
+ const hosts_1 = __importDefault(require("../../lib/hosts"));
47
+ const urls_1 = require("../../lib/urls");
48
+ const shared_1 = require("../shared");
49
+ const resolveTarget = (options) => {
50
+ if (options.port !== undefined && options.target !== undefined) {
51
+ throw new localhost_1.InputError("❌ Use either --port or --target, not both.");
52
+ }
53
+ if (options.port !== undefined)
54
+ return `http://localhost:${localhost_1.default.parsePort(options.port)}`;
55
+ if (options.target !== undefined)
56
+ return localhost_1.default.parseTarget(options.target);
57
+ throw new localhost_1.InputError("❌ Missing destination. Pass --port <port> or --target <url>.");
58
+ };
59
+ const mapping = (options) => ({
60
+ host: options.host,
61
+ target: resolveTarget(options),
62
+ insecure: options.insecure,
63
+ cors: options.cors,
64
+ });
65
+ const warnIfDown = async (entry) => {
66
+ const probe = await localhost_1.default.probe(entry.target, 1500, entry.insecure);
67
+ if (!probe.up)
68
+ (0, shared_1.print)(`⚠️ Nothing answered at ${entry.target} yet (${probe.error}). The mapping is saved anyway.`);
69
+ };
70
+ async function add(options) {
71
+ const { host, entry } = await hosts_1.default.add(mapping(options));
72
+ await (0, shared_1.ensureRunning)(options);
73
+ (0, shared_1.print)(`✅ ${(0, urls_1.urlFor)(host)} → ${entry.target}`);
74
+ if (!localhost_1.default.isLocalTarget(entry.target)) {
75
+ (0, shared_1.print)("ℹ️ Remote target: redirects to other domains will leave the .localhost name.");
76
+ }
77
+ await warnIfDown(entry);
78
+ }
79
+ async function update(options) {
80
+ const { host, entry } = await hosts_1.default.update(mapping(options));
81
+ await (0, shared_1.ensureRunning)(options);
82
+ (0, shared_1.print)(`✅ Updated ${(0, urls_1.urlFor)(host)} → ${entry.target}`);
83
+ await warnIfDown(entry);
84
+ }
85
+ async function remove(options) {
86
+ const { host } = await hosts_1.default.remove(options);
87
+ (0, shared_1.print)(`🗑️ Removed ${host}.`);
88
+ }
89
+ async function list(options) {
90
+ const hosts = Object.entries(registry_1.default.read().hosts).sort(([a], [b]) => a.localeCompare(b));
91
+ if (options.json) {
92
+ console.log(JSON.stringify(hosts.map(([host, entry]) => ({ host, url: (0, urls_1.urlFor)(host), ...entry })), null, 2));
93
+ return;
94
+ }
95
+ if (!hosts.length) {
96
+ (0, shared_1.print)("No hosts mapped. Add one: npx locadot add --host app.localhost --port 3000");
97
+ return;
98
+ }
99
+ const width = Math.max(...hosts.map(([host]) => (0, urls_1.urlFor)(host).length));
100
+ for (const [host, entry] of hosts) {
101
+ (0, shared_1.print)(`${(0, urls_1.urlFor)(host).padEnd(width)} → ${entry.target}${entry.insecure ? " (insecure)" : ""}${entry.cors ? " (cors)" : ""}`);
102
+ }
103
+ (0, shared_1.print)(`☑️ Total: ${hosts.length}.`);
104
+ }
105
+ async function clearHosts() {
106
+ await registry_1.default.mutate((registry) => (registry.hosts = {}));
107
+ (0, shared_1.print)("☑️ All hosts removed.");
108
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const hosts = __importStar(require("./hosts"));
37
+ const proxy = __importStar(require("./proxy"));
38
+ const files = __importStar(require("./files"));
39
+ const system = __importStar(require("./system"));
40
+ const tunnel = __importStar(require("./tunnel"));
41
+ const doctor_1 = require("./doctor");
42
+ const Commands = { ...hosts, ...proxy, ...files, ...system, ...tunnel, doctor: doctor_1.doctor };
43
+ exports.default = Commands;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.status = status;
7
+ exports.start = start;
8
+ exports.stop = stop;
9
+ exports.restart = restart;
10
+ exports.kill = kill;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const localhost_1 = require("../../lib/localhost");
13
+ const proxy_control_1 = __importDefault(require("../../lib/proxy-control"));
14
+ const registry_1 = __importDefault(require("../../lib/registry"));
15
+ const urls_1 = require("../../lib/urls");
16
+ const constants_1 = __importDefault(require("../../constants"));
17
+ const file_1 = __importDefault(require("../../utils/file"));
18
+ const startup_1 = __importDefault(require("../../utils/startup"));
19
+ const trust_1 = require("../../utils/trust");
20
+ const shared_1 = require("../shared");
21
+ /** Applies --port/--https-port and saves them, so `add`, `status`, `open` and boot startup use the same ports. */
22
+ const usePorts = (options) => {
23
+ if (options.port === undefined && options.httpsPort === undefined)
24
+ return false;
25
+ const parse = (value, flag) => {
26
+ if (value === undefined)
27
+ return undefined;
28
+ const port = constants_1.default.validPort(value);
29
+ if (!port)
30
+ throw new localhost_1.InputError(`❌ ${flag} must be a port between 1 and 65535, got "${value}".`);
31
+ return port;
32
+ };
33
+ const httpPort = parse(options.port, "--port") ?? constants_1.default.server.httpPort;
34
+ const httpsPort = parse(options.httpsPort, "--https-port") ?? constants_1.default.server.httpsPort;
35
+ if (httpPort === httpsPort)
36
+ throw new localhost_1.InputError(`❌ HTTP and HTTPS can't share port ${httpPort}.`);
37
+ Object.assign(constants_1.default.server, { httpPort, httpsPort });
38
+ file_1.default.ensureDir();
39
+ fs_1.default.writeFileSync(constants_1.default.paths.CONFIG_FILE, JSON.stringify({ httpPort, httpsPort }, null, 2) + "\n");
40
+ return true;
41
+ };
42
+ async function status(options) {
43
+ const info = proxy_control_1.default.running();
44
+ const hosts = Object.keys(registry_1.default.read().hosts).length;
45
+ const [trusted, startup] = await Promise.all([(0, trust_1.isCATrusted)().catch(() => undefined), startup_1.default.isEnabled()]);
46
+ const report = {
47
+ running: Boolean(info),
48
+ pid: info?.pid,
49
+ version: info?.version,
50
+ startedAt: info?.startedAt,
51
+ httpPort: info?.httpPort ?? constants_1.default.server.httpPort,
52
+ httpsPort: info?.httpsPort ?? constants_1.default.server.httpsPort,
53
+ bind: info?.bind ?? constants_1.default.server.bind,
54
+ hosts,
55
+ caTrusted: trusted,
56
+ startupEnabled: startup,
57
+ dashboard: (0, urls_1.urlFor)("localhost"),
58
+ stateDir: constants_1.default.paths.HOME,
59
+ };
60
+ if (options.json) {
61
+ console.log(JSON.stringify(report, null, 2));
62
+ return;
63
+ }
64
+ (0, shared_1.print)(info ? `🟢 Proxy running (pid ${info.pid}${info.startedAt ? `, since ${info.startedAt}` : ""})` : "🔴 Proxy not running");
65
+ (0, shared_1.print)(` Ports: http ${report.httpPort}, https ${report.httpsPort} on ${report.bind.join(", ")}`);
66
+ (0, shared_1.print)(` Hosts: ${hosts}`);
67
+ (0, shared_1.print)(` CA: ${trusted === undefined ? "unknown" : trusted ? "trusted" : "not trusted (run `locadot trust`)"}`);
68
+ (0, shared_1.print)(` Startup: ${startup ? "enabled" : "disabled"}`);
69
+ (0, shared_1.print)(` Dashboard: ${report.dashboard}`);
70
+ (0, shared_1.print)(` State dir: ${report.stateDir}`);
71
+ }
72
+ async function start(options = {}) {
73
+ const moved = usePorts(options);
74
+ const running = proxy_control_1.default.running();
75
+ // Already up on other ports: the new ones only take effect after a restart.
76
+ if (running && (running.httpPort !== constants_1.default.server.httpPort || running.httpsPort !== constants_1.default.server.httpsPort)) {
77
+ const info = await proxy_control_1.default.restart();
78
+ (0, shared_1.print)(`🔁 Proxy restarted on http ${info.httpPort}, https ${info.httpsPort} (pid ${info.pid}).`);
79
+ }
80
+ else {
81
+ const info = await proxy_control_1.default.start();
82
+ (0, shared_1.print)(running ? `☑️ Proxy already running (pid ${info.pid}).` : `🚀 Central proxy started (pid ${info.pid}).`);
83
+ if (moved || !running)
84
+ (0, shared_1.print)(` Ports: http ${info.httpPort}, https ${info.httpsPort}`);
85
+ }
86
+ (0, shared_1.print)(` Dashboard: ${(0, urls_1.urlFor)("localhost")}`);
87
+ }
88
+ async function stop() {
89
+ const stopped = await proxy_control_1.default.stop();
90
+ (0, shared_1.print)(stopped ? constants_1.default.proxyInfo.softClose : "☑️ Proxy was not running.");
91
+ }
92
+ async function restart(options = {}) {
93
+ usePorts(options);
94
+ const info = await proxy_control_1.default.restart();
95
+ (0, shared_1.print)(`☑️ Proxy restarted on http ${info.httpPort}, https ${info.httpsPort} (pid ${info.pid}).`);
96
+ }
97
+ async function kill() {
98
+ await proxy_control_1.default.kill();
99
+ (0, shared_1.print)(constants_1.default.proxyInfo.proxyClose);
100
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.open = open;
7
+ exports.trust = trust;
8
+ exports.untrust = untrust;
9
+ exports.enableStartup = enableStartup;
10
+ exports.disableStartup = disableStartup;
11
+ exports.statusStartup = statusStartup;
12
+ const os_1 = __importDefault(require("os"));
13
+ const child_process_1 = require("child_process");
14
+ const localhost_1 = __importDefault(require("../../lib/localhost"));
15
+ const urls_1 = require("../../lib/urls");
16
+ const startup_1 = __importDefault(require("../../utils/startup"));
17
+ const certs_1 = require("../../utils/certs");
18
+ const trust_1 = require("../../utils/trust");
19
+ const shared_1 = require("../shared");
20
+ async function open(host) {
21
+ const url = (0, urls_1.urlFor)(host ? localhost_1.default.requireHost(host) : "localhost");
22
+ const [command, args] = os_1.default.platform() === "win32"
23
+ ? ["cmd", ["/c", "start", "", url]]
24
+ : os_1.default.platform() === "darwin"
25
+ ? ["open", [url]]
26
+ : ["xdg-open", [url]];
27
+ try {
28
+ // windows-hide-exempt: run from the user's terminal (no popup), and hiding `cmd /c start` can hide the browser too.
29
+ (0, child_process_1.spawn)(command, args, { detached: true, stdio: "ignore" }).on("error", () => { }).unref();
30
+ }
31
+ catch { }
32
+ (0, shared_1.print)(url);
33
+ }
34
+ async function trust() {
35
+ await (0, trust_1.trustCA)();
36
+ (0, shared_1.print)(`✅ Locadot CA trusted (${(0, certs_1.caCertPath)()}). Restart your browser if it was open.`);
37
+ }
38
+ async function untrust() {
39
+ await (0, trust_1.untrustCA)();
40
+ (0, shared_1.print)("✅ Locadot CA removed from the trust store.");
41
+ }
42
+ async function enableStartup() {
43
+ await startup_1.default.enable();
44
+ }
45
+ async function disableStartup() {
46
+ await startup_1.default.disable();
47
+ }
48
+ async function statusStartup() {
49
+ (0, shared_1.print)((await startup_1.default.isEnabled()) ? "enabled" : "disabled");
50
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.installTunnel = installTunnel;
40
+ exports.tunnel = tunnel;
41
+ const http_1 = __importDefault(require("http"));
42
+ const localhost_1 = __importStar(require("../../lib/localhost"));
43
+ const hosts_1 = __importDefault(require("../../lib/hosts"));
44
+ const constants_1 = __importDefault(require("../../constants"));
45
+ const tunnel_1 = require("../../proxy/tunnel");
46
+ const shared_1 = require("../shared");
47
+ const TUNNEL_TIMEOUT_MS = 45000;
48
+ /** Tunnel state lives in the proxy process; ask it over the local read-only API. */
49
+ const tunnelRows = () => new Promise((resolve, reject) => {
50
+ const req = http_1.default.get({ host: "127.0.0.1", port: constants_1.default.server.httpPort, path: "/api/hosts", headers: { Host: "localhost" }, timeout: 5000 }, (res) => {
51
+ let body = "";
52
+ res.on("data", (chunk) => (body += chunk));
53
+ res.on("end", () => {
54
+ try {
55
+ resolve(JSON.parse(body).filter((row) => Boolean(row.tunnel?.enabled)));
56
+ }
57
+ catch {
58
+ reject(new localhost_1.InputError("❌ The proxy didn't answer /api/hosts. Is it running? Try `locadot status`."));
59
+ }
60
+ });
61
+ });
62
+ req.on("timeout", () => req.destroy(new Error("timeout")));
63
+ req.on("error", () => reject(new localhost_1.InputError("❌ The proxy isn't running. Run `locadot start`.")));
64
+ });
65
+ async function installTunnel() {
66
+ const existing = (0, tunnel_1.cloudflaredInfo)(true);
67
+ if (existing.installed) {
68
+ (0, shared_1.print)(`☑️ cloudflared ${existing.version ?? ""} is already installed (${existing.path}).`);
69
+ return;
70
+ }
71
+ (0, shared_1.print)("⬇️ Downloading cloudflared from github.com/cloudflare/cloudflared…");
72
+ const info = await (0, tunnel_1.installCloudflared)();
73
+ (0, shared_1.print)(`✅ cloudflared ${info.version ?? ""} installed at ${info.path}`);
74
+ }
75
+ /** Shares a mapping on a public trycloudflare.com URL, or lists what's shared when no host is given. */
76
+ async function tunnel(options) {
77
+ if (!options.host) {
78
+ const shared = await tunnelRows();
79
+ if (!shared.length)
80
+ (0, shared_1.print)("Nothing is shared. Share a mapping: locadot tunnel --host app.localhost");
81
+ for (const row of shared)
82
+ (0, shared_1.print)(`${row.host} → ${row.tunnel.url ?? row.tunnel.status}${row.tunnel.error ? ` (${row.tunnel.error})` : ""}`);
83
+ return;
84
+ }
85
+ const host = localhost_1.default.requireHost(options.host);
86
+ if (options.off) {
87
+ await hosts_1.default.setTunnel({ host, tunnel: false });
88
+ (0, shared_1.print)(`🔒 ${host} is no longer public.`);
89
+ return;
90
+ }
91
+ if (!(0, tunnel_1.cloudflaredInfo)(true).installed)
92
+ await installTunnel();
93
+ await hosts_1.default.setTunnel({ host, tunnel: true });
94
+ await (0, shared_1.ensureRunning)();
95
+ const deadline = Date.now() + TUNNEL_TIMEOUT_MS;
96
+ while (Date.now() < deadline) {
97
+ const state = (await tunnelRows()).find((row) => row.host === host)?.tunnel;
98
+ if (state?.status === "up") {
99
+ (0, shared_1.print)(`🌍 ${host} is public at ${state.url}`);
100
+ (0, shared_1.print)(" Anyone with the link can reach it. Stop with: locadot tunnel --host " + host + " --off");
101
+ return;
102
+ }
103
+ if (state?.status === "error")
104
+ throw new localhost_1.InputError(`❌ Tunnel for ${host} failed: ${state.error}`);
105
+ await new Promise((resolve) => setTimeout(resolve, 500));
106
+ }
107
+ throw new localhost_1.InputError(`❌ Tunnel for ${host} didn't come up within ${TUNNEL_TIMEOUT_MS / 1000}s. See \`locadot logs\`.`);
108
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ensureRunning = exports.print = void 0;
7
+ const proxy_control_1 = __importDefault(require("../lib/proxy-control"));
8
+ const logger_1 = __importDefault(require("../utils/logger"));
9
+ const print = (message) => logger_1.default.info(message);
10
+ exports.print = print;
11
+ /** The proxy only picks up mappings once running; a failure here must not be hidden. */
12
+ const ensureRunning = async (options = {}) => {
13
+ if (options.start === false)
14
+ return;
15
+ const wasRunning = proxy_control_1.default.running();
16
+ const info = await proxy_control_1.default.start();
17
+ if (!wasRunning)
18
+ (0, exports.print)(`🚀 Central proxy started (pid ${info.pid}).`);
19
+ };
20
+ exports.ensureRunning = ensureRunning;