locadot 2.0.0 → 2.1.0-beta.1
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 +39 -0
- package/README.md +52 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/remote.js +315 -0
- package/dist/cli/commands/tunnel.js +2 -1
- package/dist/cli/commands/ui.js +94 -0
- package/dist/constants/index.js +23 -0
- package/dist/constants/template.js +24 -1
- package/dist/dashboard/api.js +381 -1
- package/dist/dashboard/index.js +94 -3
- package/dist/dashboard/login.js +228 -0
- package/dist/dashboard/page.js +1814 -103
- package/dist/index.js +102 -1
- package/dist/lib/hub-config.js +46 -0
- package/dist/lib/links.js +163 -0
- package/dist/lib/localhost.js +2 -2
- package/dist/lib/proxy-control.js +2 -1
- package/dist/lib/remotes.js +370 -0
- package/dist/lib/ui-auth.js +121 -0
- package/dist/lib/words.js +32 -0
- package/dist/proxy/hub-tunnel.js +241 -0
- package/dist/proxy/hub.js +302 -0
- package/dist/proxy/remote.js +91 -0
- package/dist/proxy/request.js +4 -2
- package/dist/proxy/router.js +126 -5
- package/dist/server/index.js +51 -3
- package/dist/utils/startup.js +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const commands_1 = __importDefault(require("./cli/commands"));
|
|
|
9
9
|
const localhost_1 = require("./lib/localhost");
|
|
10
10
|
const registry_1 = require("./lib/registry");
|
|
11
11
|
const proxy_control_1 = require("./lib/proxy-control");
|
|
12
|
+
const links_1 = require("./lib/links");
|
|
13
|
+
const remotes_1 = require("./lib/remotes");
|
|
12
14
|
const logger_1 = __importDefault(require("./utils/logger"));
|
|
13
15
|
const package_json_1 = require("../package.json");
|
|
14
16
|
const program = new commander_1.Command();
|
|
@@ -20,7 +22,11 @@ const run = (fn, { exit = true } = {}) => async (...args) => {
|
|
|
20
22
|
process.exit(process.exitCode ?? 0);
|
|
21
23
|
}
|
|
22
24
|
catch (error) {
|
|
23
|
-
const known = error instanceof localhost_1.InputError ||
|
|
25
|
+
const known = error instanceof localhost_1.InputError ||
|
|
26
|
+
error instanceof proxy_control_1.ProxyError ||
|
|
27
|
+
error instanceof registry_1.RegistryError ||
|
|
28
|
+
error instanceof links_1.LinkError ||
|
|
29
|
+
error instanceof remotes_1.RemoteError;
|
|
24
30
|
logger_1.default.error(known ? error.message : `❌ ${error?.stack || error}`);
|
|
25
31
|
process.exit(1);
|
|
26
32
|
}
|
|
@@ -104,4 +110,99 @@ program.command("path:hosts").description("Show the registry file path").action(
|
|
|
104
110
|
program.command("startup:enable").description("Start locadot on boot/logon").action(run(() => commands_1.default.enableStartup()));
|
|
105
111
|
program.command("startup:disable").description("Don't start locadot on boot/logon").action(run(() => commands_1.default.disableStartup()));
|
|
106
112
|
program.command("startup:status").description("Is start on boot enabled?").action(run(() => commands_1.default.statusStartup()));
|
|
113
|
+
const roleOption = () => new commander_1.Option("-r, --role <role>", "viewer, editor or admin").choices(["viewer", "editor", "admin"]).makeOptionMandatory();
|
|
114
|
+
program
|
|
115
|
+
.command("hub")
|
|
116
|
+
.description("Show remote access status")
|
|
117
|
+
.option("--json", "Machine-readable output")
|
|
118
|
+
.action(run((options) => commands_1.default.hub(options)));
|
|
119
|
+
program
|
|
120
|
+
.command("hub:setup")
|
|
121
|
+
.description("Share this locadot on your own domain via a named Cloudflare tunnel")
|
|
122
|
+
.requiredOption("-d, --domain <domain>", "Public domain, e.g. hub.example.com")
|
|
123
|
+
.option("-t, --tunnel <name>", "Cloudflare tunnel name", "locadot")
|
|
124
|
+
.action(run((options) => commands_1.default.hubSetup(options)));
|
|
125
|
+
program
|
|
126
|
+
.command("hub:quick")
|
|
127
|
+
.description("Share this locadot on a random trycloudflare.com URL (no account, URL changes on restart)")
|
|
128
|
+
.action(run(() => commands_1.default.hubQuick()));
|
|
129
|
+
program
|
|
130
|
+
.command("hub:manual")
|
|
131
|
+
.description("Share this locadot behind a URL something else already forwards (ngrok, a reverse proxy, tests)")
|
|
132
|
+
.requiredOption("-u, --url <url>", "Public base URL that forwards to this proxy's HTTP port")
|
|
133
|
+
.action(run((options) => commands_1.default.hubManual(options)));
|
|
134
|
+
program.command("hub:off").description("Turn off remote access").action(run(() => commands_1.default.hubOff()));
|
|
135
|
+
program
|
|
136
|
+
.command("hub:localhost <onOrOff>")
|
|
137
|
+
.description("Sender: let admin peers reach any port on this machine's localhost (on/off)")
|
|
138
|
+
.action(run((value) => commands_1.default.hubLocalhost(value)));
|
|
139
|
+
program
|
|
140
|
+
.command("ui:password")
|
|
141
|
+
.description("Set or reset the dashboard password (prompts; hashed with scrypt). The API token keeps working for scripts")
|
|
142
|
+
.option("--off", "Remove the password; the dashboard is open to anyone on this machine again")
|
|
143
|
+
.option("--stdin", "Read the password from stdin instead of prompting")
|
|
144
|
+
.action(run((options) => commands_1.default.uiPassword(options)));
|
|
145
|
+
program
|
|
146
|
+
.command("share")
|
|
147
|
+
.description("Create a one-time pairing code for someone to connect to this locadot")
|
|
148
|
+
.addOption(roleOption())
|
|
149
|
+
.option("--hosts <hosts>", "Comma separated hosts the viewer may see (viewer role only)")
|
|
150
|
+
.action(run((options) => commands_1.default.share(options)));
|
|
151
|
+
program
|
|
152
|
+
.command("peers")
|
|
153
|
+
.description("List everyone paired with this locadot")
|
|
154
|
+
.option("--json", "Machine-readable output")
|
|
155
|
+
.action(run((options) => commands_1.default.peers(options)));
|
|
156
|
+
program
|
|
157
|
+
.command("peers:role <id> <role>")
|
|
158
|
+
.description("Change a peer's role")
|
|
159
|
+
.option("--hosts <hosts>", "Comma separated hosts (viewer role only)")
|
|
160
|
+
.action(run((id, role, options) => commands_1.default.peersRole(id, role, options)));
|
|
161
|
+
program.command("peers:revoke <id>").description("Revoke a peer").action(run((id) => commands_1.default.peersRevoke(id)));
|
|
162
|
+
program
|
|
163
|
+
.command("connect <string>")
|
|
164
|
+
.description("Connect to a remote locadot using its pairing string")
|
|
165
|
+
.option("--name <name>", "Local name for this remote (defaults to the sender's hostname)")
|
|
166
|
+
.option("--domain <domain>", "Local domain for the sender's localhost, e.g. dev (admin only; random if omitted)")
|
|
167
|
+
.action(run((value, options) => commands_1.default.connect(value, options)));
|
|
168
|
+
program
|
|
169
|
+
.command("remotes")
|
|
170
|
+
.description("List remotes this locadot is connected to")
|
|
171
|
+
.option("--json", "Machine-readable output")
|
|
172
|
+
.action(run((options) => commands_1.default.remotes(options)));
|
|
173
|
+
program.command("remote:sync <name>").description("Refresh role and available hosts for a remote").action(run((name) => commands_1.default.remoteSync(name)));
|
|
174
|
+
program
|
|
175
|
+
.command("remote:domain <name> [domain]")
|
|
176
|
+
.description("Set the local domain for a remote's localhost (admin only); no domain picks a random one")
|
|
177
|
+
.option("--off", "Remove the domain")
|
|
178
|
+
.action(run((name, domain, options) => commands_1.default.remoteDomain(name, domain, options)));
|
|
179
|
+
program
|
|
180
|
+
.command("remote:alias <name> <remoteHost> <localHost>")
|
|
181
|
+
.description("Map a local .localhost name to a host on a remote")
|
|
182
|
+
.action(run((name, remoteHost, localHost) => commands_1.default.remoteAlias(name, remoteHost, localHost)));
|
|
183
|
+
program
|
|
184
|
+
.command("remote:url <name> <url>")
|
|
185
|
+
.description("Update a remote's URL (e.g. after its quick tunnel changed)")
|
|
186
|
+
.action(run((name, url) => commands_1.default.remoteUrl(name, url)));
|
|
187
|
+
program
|
|
188
|
+
.command("remote:add <name>")
|
|
189
|
+
.description("Add a mapping on a remote")
|
|
190
|
+
.addOption(hostOption())
|
|
191
|
+
.requiredOption("-t, --target <url>", "Any upstream: 3000, 127.0.0.1:8080, http://192.168.1.5:8080, https://google.com")
|
|
192
|
+
.option("-k, --insecure", "Don't verify the TLS certificate of an https target")
|
|
193
|
+
.option("--cors", "Send Origin/Referer as the target's own and let any origin call this domain")
|
|
194
|
+
.action(run((name, options) => commands_1.default.remoteAdd(name, options)));
|
|
195
|
+
program
|
|
196
|
+
.command("remote:update <name>")
|
|
197
|
+
.description("Update a mapping on a remote")
|
|
198
|
+
.addOption(hostOption())
|
|
199
|
+
.option("-t, --target <url>", "New upstream")
|
|
200
|
+
.option("--cors", "Send Origin/Referer as the target's own and let any origin call this domain")
|
|
201
|
+
.option("--no-cors", "Turn --cors off again")
|
|
202
|
+
.action(run((name, options) => commands_1.default.remoteUpdate(name, options)));
|
|
203
|
+
program
|
|
204
|
+
.command("remote:rm <name> <host>")
|
|
205
|
+
.description("Remove a mapping on a remote")
|
|
206
|
+
.action(run((name, host) => commands_1.default.remoteRm(name, host)));
|
|
207
|
+
program.command("disconnect <name>").description("Disconnect from a remote").action(run((name) => commands_1.default.disconnect(name)));
|
|
107
208
|
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const constants_1 = __importDefault(require("../constants"));
|
|
9
|
+
const file_1 = __importDefault(require("../utils/file"));
|
|
10
|
+
let cache;
|
|
11
|
+
/** Persists how the sender is reachable (`HUB_FILE`). No secrets in here; see links.ts for those. */
|
|
12
|
+
class HubConfigStore {
|
|
13
|
+
static read() {
|
|
14
|
+
const raw = file_1.default.read("HUB_FILE");
|
|
15
|
+
if (!raw)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(raw);
|
|
19
|
+
return data && typeof data === "object" ? data : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Same as read(), but skips the parse when the file's mtime hasn't changed. Safe for the request hot path (classify()). */
|
|
26
|
+
static readCached() {
|
|
27
|
+
let mtimeMs;
|
|
28
|
+
try {
|
|
29
|
+
mtimeMs = fs_1.default.statSync(path_1.default.resolve(constants_1.default.paths.HUB_FILE)).mtimeMs;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
cache = undefined;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!cache || cache.mtimeMs !== mtimeMs)
|
|
36
|
+
cache = { mtimeMs, value: HubConfigStore.read() };
|
|
37
|
+
return cache.value;
|
|
38
|
+
}
|
|
39
|
+
static write(config) {
|
|
40
|
+
file_1.default.writeAtomic("HUB_FILE", JSON.stringify(config, null, 2));
|
|
41
|
+
}
|
|
42
|
+
static clear() {
|
|
43
|
+
file_1.default.remove("HUB_FILE");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.default = HubConfigStore;
|
|
@@ -0,0 +1,163 @@
|
|
|
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.LinkError = exports.INVITE_TTL_MS = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const constants_1 = __importDefault(require("../constants"));
|
|
10
|
+
const file_1 = __importDefault(require("../utils/file"));
|
|
11
|
+
exports.INVITE_TTL_MS = 5 * 60000;
|
|
12
|
+
class LinkError extends Error {
|
|
13
|
+
constructor(status, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.LinkError = LinkError;
|
|
19
|
+
const sha256Hex = (value) => crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
20
|
+
/** Constant-time compare of a stored hex digest against a fresh secret. */
|
|
21
|
+
const matchesHash = (storedHex, secret) => {
|
|
22
|
+
const stored = Buffer.from(storedHex, "hex");
|
|
23
|
+
const candidate = Buffer.from(sha256Hex(secret), "hex");
|
|
24
|
+
return stored.length === candidate.length && crypto_1.default.timingSafeEqual(stored, candidate);
|
|
25
|
+
};
|
|
26
|
+
const load = () => {
|
|
27
|
+
const raw = file_1.default.read("LINKS_FILE");
|
|
28
|
+
if (!raw)
|
|
29
|
+
return { invites: [], peers: [] };
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(raw);
|
|
32
|
+
return {
|
|
33
|
+
invites: Array.isArray(data?.invites) ? data.invites : [],
|
|
34
|
+
peers: Array.isArray(data?.peers) ? data.peers : [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { invites: [], peers: [] };
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const save = (data) => {
|
|
42
|
+
file_1.default.writeAtomic("LINKS_FILE", JSON.stringify(data, null, 2));
|
|
43
|
+
fs_1.default.chmodSync(constants_1.default.paths.LINKS_FILE, 0o600);
|
|
44
|
+
};
|
|
45
|
+
const CODE_PATTERN = /^lnk_([0-9a-f]{8})\.(.+)$/i;
|
|
46
|
+
const TOKEN_PATTERN = /^lpt_([0-9a-f]{8})\.(.+)$/i;
|
|
47
|
+
class Links {
|
|
48
|
+
static createInvite(input) {
|
|
49
|
+
const id = crypto_1.default.randomBytes(4).toString("hex");
|
|
50
|
+
const secret = crypto_1.default.randomBytes(32).toString("base64url");
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const invite = {
|
|
53
|
+
id,
|
|
54
|
+
codeHash: sha256Hex(secret),
|
|
55
|
+
role: input.role,
|
|
56
|
+
hosts: input.hosts,
|
|
57
|
+
createdAt: now.toISOString(),
|
|
58
|
+
expiresAt: new Date(now.getTime() + exports.INVITE_TTL_MS).toISOString(),
|
|
59
|
+
};
|
|
60
|
+
const data = load();
|
|
61
|
+
data.invites.push(invite);
|
|
62
|
+
save(data);
|
|
63
|
+
return { invite, code: `lnk_${id}.${secret}` };
|
|
64
|
+
}
|
|
65
|
+
static inviteString(baseUrl, code) {
|
|
66
|
+
return `${baseUrl.replace(/\/$/, "")}/#${code}`;
|
|
67
|
+
}
|
|
68
|
+
/** One-time: the invite is removed once a matching id is resolved, success or not. */
|
|
69
|
+
static redeem(code, name) {
|
|
70
|
+
const match = CODE_PATTERN.exec(String(code || "").trim());
|
|
71
|
+
if (!match)
|
|
72
|
+
throw new LinkError(401, "Invalid or expired code.");
|
|
73
|
+
const [, id, secret] = match;
|
|
74
|
+
const data = load();
|
|
75
|
+
const index = data.invites.findIndex((invite) => invite.id === id);
|
|
76
|
+
if (index === -1)
|
|
77
|
+
throw new LinkError(401, "Invalid or expired code.");
|
|
78
|
+
const invite = data.invites[index];
|
|
79
|
+
data.invites.splice(index, 1);
|
|
80
|
+
save(data);
|
|
81
|
+
if (Date.parse(invite.expiresAt) < Date.now())
|
|
82
|
+
throw new LinkError(401, "Invalid or expired code.");
|
|
83
|
+
if (!matchesHash(invite.codeHash, secret))
|
|
84
|
+
throw new LinkError(401, "Invalid or expired code.");
|
|
85
|
+
const peerId = crypto_1.default.randomBytes(4).toString("hex");
|
|
86
|
+
const tokenSecret = crypto_1.default.randomBytes(32).toString("base64url");
|
|
87
|
+
const peer = {
|
|
88
|
+
id: peerId,
|
|
89
|
+
name: String(name || "peer"),
|
|
90
|
+
role: invite.role,
|
|
91
|
+
hosts: invite.hosts,
|
|
92
|
+
tokenHash: sha256Hex(tokenSecret),
|
|
93
|
+
createdAt: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
const fresh = load();
|
|
96
|
+
fresh.peers.push(peer);
|
|
97
|
+
save(fresh);
|
|
98
|
+
return { peer, token: `lpt_${peerId}.${tokenSecret}` };
|
|
99
|
+
}
|
|
100
|
+
/** Updates lastSeen at most once a minute so every proxied request doesn't rewrite the file. */
|
|
101
|
+
static authenticate(token) {
|
|
102
|
+
if (!token)
|
|
103
|
+
return undefined;
|
|
104
|
+
const match = TOKEN_PATTERN.exec(token.trim());
|
|
105
|
+
if (!match)
|
|
106
|
+
return undefined;
|
|
107
|
+
const [, id, secret] = match;
|
|
108
|
+
const data = load();
|
|
109
|
+
const peer = data.peers.find((p) => p.id === id);
|
|
110
|
+
if (!peer || !matchesHash(peer.tokenHash, secret))
|
|
111
|
+
return undefined;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (!peer.lastSeen || now - Date.parse(peer.lastSeen) >= 60000) {
|
|
114
|
+
peer.lastSeen = new Date(now).toISOString();
|
|
115
|
+
save(data);
|
|
116
|
+
}
|
|
117
|
+
return peer;
|
|
118
|
+
}
|
|
119
|
+
static list() {
|
|
120
|
+
const data = load();
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const invites = data.invites.filter((invite) => Date.parse(invite.expiresAt) >= now);
|
|
123
|
+
if (invites.length !== data.invites.length)
|
|
124
|
+
save({ ...data, invites });
|
|
125
|
+
return { invites, peers: data.peers };
|
|
126
|
+
}
|
|
127
|
+
static setRole(peerId, role, hosts) {
|
|
128
|
+
const data = load();
|
|
129
|
+
const peer = data.peers.find((p) => p.id === peerId);
|
|
130
|
+
if (!peer)
|
|
131
|
+
throw new LinkError(404, "Peer not found.");
|
|
132
|
+
peer.role = role;
|
|
133
|
+
peer.hosts = hosts;
|
|
134
|
+
save(data);
|
|
135
|
+
return peer;
|
|
136
|
+
}
|
|
137
|
+
static revoke(peerId) {
|
|
138
|
+
const data = load();
|
|
139
|
+
const peers = data.peers.filter((p) => p.id !== peerId);
|
|
140
|
+
if (peers.length !== data.peers.length)
|
|
141
|
+
save({ ...data, peers });
|
|
142
|
+
}
|
|
143
|
+
static revokeInvite(id) {
|
|
144
|
+
const data = load();
|
|
145
|
+
const invites = data.invites.filter((invite) => invite.id !== id);
|
|
146
|
+
if (invites.length !== data.invites.length)
|
|
147
|
+
save({ ...data, invites });
|
|
148
|
+
}
|
|
149
|
+
static can(peer, permission) {
|
|
150
|
+
if (permission === "read")
|
|
151
|
+
return true;
|
|
152
|
+
if (permission === "write")
|
|
153
|
+
return peer.role === "editor" || peer.role === "admin";
|
|
154
|
+
return peer.role === "admin"; // delete, settings
|
|
155
|
+
}
|
|
156
|
+
static visibleHosts(peer, all) {
|
|
157
|
+
if (peer.role !== "viewer")
|
|
158
|
+
return all;
|
|
159
|
+
const mine = new Set(peer.hosts || []);
|
|
160
|
+
return all.filter((host) => mine.has(host));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.default = Links;
|
package/dist/lib/localhost.js
CHANGED
|
@@ -76,7 +76,7 @@ class Localhost {
|
|
|
76
76
|
return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(hostname) || hostname.endsWith(".localhost");
|
|
77
77
|
}
|
|
78
78
|
/** Any HTTP response (even 404/500) counts as up: the upstream is reachable. */
|
|
79
|
-
static probe(target, timeoutMs = 1500, insecure = false) {
|
|
79
|
+
static probe(target, timeoutMs = 1500, insecure = false, headers = {}) {
|
|
80
80
|
const started = Date.now();
|
|
81
81
|
return new Promise((resolve) => {
|
|
82
82
|
let url;
|
|
@@ -87,7 +87,7 @@ class Localhost {
|
|
|
87
87
|
return resolve({ up: false, error: "invalid target" });
|
|
88
88
|
}
|
|
89
89
|
const client = url.protocol === "https:" ? https_1.default : http_1.default;
|
|
90
|
-
const req = client.request(url, { method: "HEAD", timeout: timeoutMs, rejectUnauthorized: !insecure }, (res) => {
|
|
90
|
+
const req = client.request(url, { method: "HEAD", timeout: timeoutMs, rejectUnauthorized: !insecure, headers }, (res) => {
|
|
91
91
|
res.resume();
|
|
92
92
|
resolve({ up: true, status: res.statusCode, ms: Date.now() - started });
|
|
93
93
|
});
|
|
@@ -52,14 +52,15 @@ class ProxyHandler {
|
|
|
52
52
|
const child = (0, child_process_1.spawn)(command, args, {
|
|
53
53
|
detached: true,
|
|
54
54
|
stdio: ["ignore", logFd, logFd],
|
|
55
|
+
windowsHide: true,
|
|
55
56
|
env: {
|
|
56
57
|
...process.env,
|
|
57
58
|
LOCADOT_HOME: constants_1.default.paths.HOME,
|
|
58
59
|
LOCADOT_ROLE: "proxy",
|
|
59
60
|
LOCADOT_HTTP_PORT: String(constants_1.default.server.httpPort),
|
|
60
61
|
LOCADOT_HTTPS_PORT: String(constants_1.default.server.httpsPort),
|
|
62
|
+
LOCADOT_USER_PORTS: constants_1.default.userPortEnv(),
|
|
61
63
|
},
|
|
62
|
-
windowsHide: true,
|
|
63
64
|
});
|
|
64
65
|
fs_1.default.closeSync(logFd);
|
|
65
66
|
let exitCode = null;
|