portless 0.15.1 → 0.15.3
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/README.md +4 -2
- package/dist/{chunk-SD2PIWJU.js → chunk-JSJUKQRJ.js} +77 -20
- package/dist/cli.js +380 -285
- package/dist/index.d.ts +26 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
checkHostResolution,
|
|
8
8
|
cleanHostsFile,
|
|
9
9
|
createHttpRedirectServer,
|
|
10
|
+
createLoopbackConnection,
|
|
10
11
|
createProxyServer,
|
|
11
12
|
fixOwnership,
|
|
12
13
|
formatUrl,
|
|
@@ -15,9 +16,10 @@ import {
|
|
|
15
16
|
isProcessAlive,
|
|
16
17
|
parseHostname,
|
|
17
18
|
parseHostnames,
|
|
19
|
+
resolveUserHome,
|
|
18
20
|
shouldAutoSyncHosts,
|
|
19
21
|
syncHostsFile
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-JSJUKQRJ.js";
|
|
21
23
|
|
|
22
24
|
// src/colors.ts
|
|
23
25
|
function supportsColor() {
|
|
@@ -43,18 +45,96 @@ var gray = dim;
|
|
|
43
45
|
var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
|
|
44
46
|
|
|
45
47
|
// src/cli.ts
|
|
46
|
-
import * as
|
|
48
|
+
import * as fs10 from "fs";
|
|
47
49
|
import * as path9 from "path";
|
|
48
50
|
import { spawn as spawn4, spawnSync as spawnSync5 } from "child_process";
|
|
49
51
|
import { StringDecoder } from "string_decoder";
|
|
50
52
|
|
|
51
53
|
// src/certs.ts
|
|
52
|
-
import * as
|
|
54
|
+
import * as fs2 from "fs";
|
|
53
55
|
import * as path from "path";
|
|
54
|
-
import * as
|
|
56
|
+
import * as crypto2 from "crypto";
|
|
55
57
|
import * as tls from "tls";
|
|
56
|
-
import { execFile as execFileCb, execFileSync } from "child_process";
|
|
58
|
+
import { execFile as execFileCb, execFileSync as execFileSync2 } from "child_process";
|
|
57
59
|
import { promisify } from "util";
|
|
60
|
+
|
|
61
|
+
// src/windows-ca.ts
|
|
62
|
+
import * as crypto from "crypto";
|
|
63
|
+
import * as fs from "fs";
|
|
64
|
+
import * as os from "os";
|
|
65
|
+
import { execFileSync } from "child_process";
|
|
66
|
+
var WINDOWS_COMMAND_TIMEOUT_MS = 3e4;
|
|
67
|
+
var commandOptions = {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
timeout: WINDOWS_COMMAND_TIMEOUT_MS,
|
|
70
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
71
|
+
};
|
|
72
|
+
var defaultRunner = (command, args, options) => execFileSync(command, args, options);
|
|
73
|
+
function isWSL(options = {}) {
|
|
74
|
+
const platform = options.platform ?? process.platform;
|
|
75
|
+
if (platform !== "linux") return false;
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
78
|
+
const release2 = options.release ?? os.release();
|
|
79
|
+
return release2.toLowerCase().includes("microsoft");
|
|
80
|
+
}
|
|
81
|
+
function wslWindowsCAStoreOptions(run = defaultRunner) {
|
|
82
|
+
const command = run(
|
|
83
|
+
"wslpath",
|
|
84
|
+
["-u", String.raw`C:\Windows\System32\certutil.exe`],
|
|
85
|
+
commandOptions
|
|
86
|
+
).trim();
|
|
87
|
+
return {
|
|
88
|
+
command,
|
|
89
|
+
certificatePath: (certificatePath) => run("wslpath", ["-w", certificatePath], commandOptions).trim(),
|
|
90
|
+
run
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function certificateFingerprint(certificatePath) {
|
|
94
|
+
const certificate = new crypto.X509Certificate(fs.readFileSync(certificatePath));
|
|
95
|
+
return certificate.fingerprint.replace(/:/g, "").toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
function storeOptions(options) {
|
|
98
|
+
return {
|
|
99
|
+
command: options.command ?? "certutil",
|
|
100
|
+
certificatePath: options.certificatePath ?? ((certificatePath) => certificatePath),
|
|
101
|
+
run: options.run ?? defaultRunner
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function storeContainsFingerprint(fingerprint, resolved) {
|
|
105
|
+
const listing = resolved.run(resolved.command, ["-store", "-user", "Root"], commandOptions);
|
|
106
|
+
return listing.replace(/\s/g, "").toLowerCase().includes(fingerprint);
|
|
107
|
+
}
|
|
108
|
+
function isWindowsCATrusted(caCertPath, options = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const resolved = storeOptions(options);
|
|
111
|
+
const fingerprint = certificateFingerprint(caCertPath);
|
|
112
|
+
return storeContainsFingerprint(fingerprint, resolved);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function trustWindowsCA(caCertPath, options = {}) {
|
|
118
|
+
const resolved = storeOptions(options);
|
|
119
|
+
resolved.run(
|
|
120
|
+
resolved.command,
|
|
121
|
+
["-addstore", "-user", "Root", resolved.certificatePath(caCertPath)],
|
|
122
|
+
commandOptions
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
function untrustWindowsCA(caCertPath, options = {}) {
|
|
126
|
+
try {
|
|
127
|
+
const resolved = storeOptions(options);
|
|
128
|
+
const fingerprint = certificateFingerprint(caCertPath);
|
|
129
|
+
if (!storeContainsFingerprint(fingerprint, resolved)) return { removed: true };
|
|
130
|
+
resolved.run(resolved.command, ["-delstore", "-user", "Root", fingerprint], commandOptions);
|
|
131
|
+
return storeContainsFingerprint(fingerprint, resolved) ? { removed: false, error: "certutil could not remove the portless CA from Root" } : { removed: true };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { removed: false, error: error instanceof Error ? error.message : String(error) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/certs.ts
|
|
58
138
|
var CA_VALIDITY_DAYS = 3650;
|
|
59
139
|
var SERVER_VALIDITY_DAYS = 365;
|
|
60
140
|
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -68,9 +148,10 @@ var CA_CERT_FILE = "ca.pem";
|
|
|
68
148
|
var SERVER_KEY_FILE = "server-key.pem";
|
|
69
149
|
var SERVER_CERT_FILE = "server.pem";
|
|
70
150
|
var CA_TRUST_MARKER = "ca.trusted";
|
|
151
|
+
var CA_TRUST_REFRESH_PENDING = "ca.trust-refresh-pending";
|
|
71
152
|
function fileExists(filePath) {
|
|
72
153
|
try {
|
|
73
|
-
|
|
154
|
+
fs2.accessSync(filePath, fs2.constants.R_OK);
|
|
74
155
|
return true;
|
|
75
156
|
} catch {
|
|
76
157
|
return false;
|
|
@@ -79,30 +160,38 @@ function fileExists(filePath) {
|
|
|
79
160
|
function caFingerprint(stateDir) {
|
|
80
161
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
81
162
|
try {
|
|
82
|
-
const pem =
|
|
83
|
-
return
|
|
163
|
+
const pem = fs2.readFileSync(caCertPath);
|
|
164
|
+
return crypto2.createHash("sha256").update(pem).digest("hex");
|
|
84
165
|
} catch {
|
|
85
166
|
return null;
|
|
86
167
|
}
|
|
87
168
|
}
|
|
88
169
|
function readTrustMarker(stateDir) {
|
|
89
170
|
try {
|
|
90
|
-
const value =
|
|
171
|
+
const value = fs2.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
|
|
91
172
|
return value || null;
|
|
92
173
|
} catch {
|
|
93
174
|
return null;
|
|
94
175
|
}
|
|
95
176
|
}
|
|
177
|
+
function clearTrustRefreshPending(stateDir) {
|
|
178
|
+
try {
|
|
179
|
+
fs2.unlinkSync(path.join(stateDir, CA_TRUST_REFRESH_PENDING));
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
96
183
|
function writeTrustMarker(stateDir) {
|
|
97
184
|
const fp = caFingerprint(stateDir);
|
|
98
185
|
if (fp) {
|
|
99
|
-
|
|
186
|
+
clearTrustRefreshPending(stateDir);
|
|
187
|
+
const marker = isWSL() ? `wsl:${fp}` : fp;
|
|
188
|
+
fs2.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), marker + "\n");
|
|
100
189
|
fixOwnership(path.join(stateDir, CA_TRUST_MARKER));
|
|
101
190
|
}
|
|
102
191
|
}
|
|
103
192
|
function clearTrustMarker(stateDir) {
|
|
104
193
|
try {
|
|
105
|
-
|
|
194
|
+
fs2.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
|
|
106
195
|
} catch {
|
|
107
196
|
}
|
|
108
197
|
}
|
|
@@ -142,8 +231,8 @@ function opensslErrorMessage() {
|
|
|
142
231
|
}
|
|
143
232
|
function isCertValid(certPath) {
|
|
144
233
|
try {
|
|
145
|
-
const pem =
|
|
146
|
-
const cert = new
|
|
234
|
+
const pem = fs2.readFileSync(certPath, "utf-8");
|
|
235
|
+
const cert = new crypto2.X509Certificate(pem);
|
|
147
236
|
const expiry = new Date(cert.validTo).getTime();
|
|
148
237
|
return Date.now() + EXPIRY_BUFFER_MS < expiry;
|
|
149
238
|
} catch {
|
|
@@ -172,7 +261,7 @@ function isCertSignatureStrong(certPath) {
|
|
|
172
261
|
function openssl(args, options) {
|
|
173
262
|
try {
|
|
174
263
|
const extraEnv = getOpensslEnv();
|
|
175
|
-
return
|
|
264
|
+
return execFileSync2("openssl", args, {
|
|
176
265
|
encoding: "utf-8",
|
|
177
266
|
timeout: OPENSSL_TIMEOUT_MS,
|
|
178
267
|
input: options?.input,
|
|
@@ -225,8 +314,8 @@ function generateCA(stateDir) {
|
|
|
225
314
|
"-addext",
|
|
226
315
|
"keyUsage=critical,keyCertSign,cRLSign"
|
|
227
316
|
]);
|
|
228
|
-
|
|
229
|
-
|
|
317
|
+
fs2.chmodSync(keyPath, 384);
|
|
318
|
+
fs2.chmodSync(certPath, 420);
|
|
230
319
|
fixOwnership(keyPath, certPath);
|
|
231
320
|
clearTrustMarker(stateDir);
|
|
232
321
|
return { certPath, keyPath };
|
|
@@ -241,7 +330,7 @@ function generateServerCert(stateDir) {
|
|
|
241
330
|
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
242
331
|
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
243
332
|
const sans = ["DNS:localhost", "DNS:*.localhost", "DNS:*.local"];
|
|
244
|
-
|
|
333
|
+
fs2.writeFileSync(
|
|
245
334
|
extPath,
|
|
246
335
|
[
|
|
247
336
|
"authorityKeyIdentifier=keyid,issuer",
|
|
@@ -253,9 +342,9 @@ function generateServerCert(stateDir) {
|
|
|
253
342
|
);
|
|
254
343
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
255
344
|
if (!fileExists(srlPath)) {
|
|
256
|
-
|
|
345
|
+
fs2.writeFileSync(
|
|
257
346
|
srlPath,
|
|
258
|
-
|
|
347
|
+
crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
259
348
|
);
|
|
260
349
|
}
|
|
261
350
|
openssl([
|
|
@@ -279,12 +368,12 @@ function generateServerCert(stateDir) {
|
|
|
279
368
|
]);
|
|
280
369
|
for (const tmp of [csrPath, extPath]) {
|
|
281
370
|
try {
|
|
282
|
-
|
|
371
|
+
fs2.unlinkSync(tmp);
|
|
283
372
|
} catch {
|
|
284
373
|
}
|
|
285
374
|
}
|
|
286
|
-
|
|
287
|
-
|
|
375
|
+
fs2.chmodSync(serverKeyPath, 384);
|
|
376
|
+
fs2.chmodSync(serverCertPath, 420);
|
|
288
377
|
fixOwnership(serverKeyPath, serverCertPath);
|
|
289
378
|
return { certPath: serverCertPath, keyPath: serverKeyPath };
|
|
290
379
|
}
|
|
@@ -315,36 +404,30 @@ function isCATrusted(stateDir) {
|
|
|
315
404
|
const marker = readTrustMarker(stateDir);
|
|
316
405
|
if (marker) {
|
|
317
406
|
const fp = caFingerprint(stateDir);
|
|
318
|
-
|
|
407
|
+
const expected = fp && isWSL() ? `wsl:${fp}` : fp;
|
|
408
|
+
if (expected && marker === expected) return true;
|
|
319
409
|
}
|
|
320
410
|
if (process.platform === "darwin") {
|
|
321
411
|
return isCATrustedMacOS(caCertPath);
|
|
322
412
|
} else if (process.platform === "linux") {
|
|
323
|
-
|
|
413
|
+
if (!isCATrustedLinux(stateDir)) return false;
|
|
414
|
+
if (!isWSL()) return true;
|
|
415
|
+
try {
|
|
416
|
+
return isWindowsCATrusted(caCertPath, wslWindowsCAStoreOptions());
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
324
420
|
} else if (process.platform === "win32") {
|
|
325
|
-
return
|
|
421
|
+
return isWindowsCATrusted(caCertPath);
|
|
326
422
|
}
|
|
327
423
|
return false;
|
|
328
424
|
}
|
|
329
|
-
function isCATrustedWindows(caCertPath) {
|
|
330
|
-
try {
|
|
331
|
-
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
332
|
-
const result = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
333
|
-
encoding: "utf-8",
|
|
334
|
-
timeout: 1e4,
|
|
335
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
336
|
-
});
|
|
337
|
-
return result.replace(/\s/g, "").toLowerCase().includes(fingerprint);
|
|
338
|
-
} catch {
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
425
|
function isCATrustedMacOS(caCertPath) {
|
|
343
426
|
try {
|
|
344
427
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
345
428
|
const sudoUser = process.env.SUDO_USER;
|
|
346
429
|
if (isRoot && sudoUser) {
|
|
347
|
-
|
|
430
|
+
execFileSync2(
|
|
348
431
|
"sudo",
|
|
349
432
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
350
433
|
{
|
|
@@ -353,7 +436,7 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
353
436
|
}
|
|
354
437
|
);
|
|
355
438
|
} else {
|
|
356
|
-
|
|
439
|
+
execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
357
440
|
stdio: "pipe",
|
|
358
441
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
359
442
|
});
|
|
@@ -365,7 +448,7 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
365
448
|
}
|
|
366
449
|
function loginKeychainPath() {
|
|
367
450
|
try {
|
|
368
|
-
const result =
|
|
451
|
+
const result = execFileSync2("security", ["default-keychain"], {
|
|
369
452
|
encoding: "utf-8",
|
|
370
453
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
371
454
|
}).trim();
|
|
@@ -396,7 +479,7 @@ var LINUX_CA_TRUST_CONFIGS = {
|
|
|
396
479
|
};
|
|
397
480
|
function detectLinuxDistro() {
|
|
398
481
|
try {
|
|
399
|
-
const osRelease =
|
|
482
|
+
const osRelease = fs2.readFileSync("/etc/os-release", "utf-8").toLowerCase();
|
|
400
483
|
if (osRelease.includes("arch")) return "arch";
|
|
401
484
|
if (osRelease.includes("fedora") || osRelease.includes("rhel") || osRelease.includes("centos"))
|
|
402
485
|
return "fedora";
|
|
@@ -406,8 +489,8 @@ function detectLinuxDistro() {
|
|
|
406
489
|
}
|
|
407
490
|
for (const [distro, config] of Object.entries(LINUX_CA_TRUST_CONFIGS)) {
|
|
408
491
|
try {
|
|
409
|
-
|
|
410
|
-
if (
|
|
492
|
+
execFileSync2("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
|
|
493
|
+
if (fs2.existsSync(path.dirname(config.certDir))) return distro;
|
|
411
494
|
} catch {
|
|
412
495
|
}
|
|
413
496
|
}
|
|
@@ -417,13 +500,12 @@ function getLinuxCATrustConfig() {
|
|
|
417
500
|
const distro = detectLinuxDistro();
|
|
418
501
|
return LINUX_CA_TRUST_CONFIGS[distro ?? "debian"];
|
|
419
502
|
}
|
|
420
|
-
function isCATrustedLinux(stateDir) {
|
|
421
|
-
const config = getLinuxCATrustConfig();
|
|
503
|
+
function isCATrustedLinux(stateDir, config = getLinuxCATrustConfig()) {
|
|
422
504
|
const systemCertPath = path.join(config.certDir, "portless-ca.crt");
|
|
423
505
|
if (!fileExists(systemCertPath)) return false;
|
|
424
506
|
try {
|
|
425
|
-
const ours =
|
|
426
|
-
const installed =
|
|
507
|
+
const ours = fs2.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
508
|
+
const installed = fs2.readFileSync(systemCertPath, "utf-8").trim();
|
|
427
509
|
return ours === installed;
|
|
428
510
|
} catch {
|
|
429
511
|
return false;
|
|
@@ -438,8 +520,8 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
438
520
|
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
439
521
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
440
522
|
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
441
|
-
if (!
|
|
442
|
-
await
|
|
523
|
+
if (!fs2.existsSync(hostDir)) {
|
|
524
|
+
await fs2.promises.mkdir(hostDir, { recursive: true, mode: 493 });
|
|
443
525
|
fixOwnership(hostDir);
|
|
444
526
|
}
|
|
445
527
|
const safeName = sanitizeHostForFilename(hostname);
|
|
@@ -455,7 +537,7 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
455
537
|
if (parts.length >= 2) {
|
|
456
538
|
sans.push(`DNS:*.${parts.slice(1).join(".")}`);
|
|
457
539
|
}
|
|
458
|
-
await
|
|
540
|
+
await fs2.promises.writeFile(
|
|
459
541
|
extPath,
|
|
460
542
|
[
|
|
461
543
|
"authorityKeyIdentifier=keyid,issuer",
|
|
@@ -466,10 +548,10 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
466
548
|
].join("\n") + "\n"
|
|
467
549
|
);
|
|
468
550
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
469
|
-
if (!
|
|
470
|
-
await
|
|
551
|
+
if (!fs2.existsSync(srlPath)) {
|
|
552
|
+
await fs2.promises.writeFile(
|
|
471
553
|
srlPath,
|
|
472
|
-
|
|
554
|
+
crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
473
555
|
);
|
|
474
556
|
}
|
|
475
557
|
await opensslAsync([
|
|
@@ -493,12 +575,12 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
493
575
|
]);
|
|
494
576
|
for (const tmp of [csrPath, extPath]) {
|
|
495
577
|
try {
|
|
496
|
-
await
|
|
578
|
+
await fs2.promises.unlink(tmp);
|
|
497
579
|
} catch {
|
|
498
580
|
}
|
|
499
581
|
}
|
|
500
|
-
await
|
|
501
|
-
await
|
|
582
|
+
await fs2.promises.chmod(keyPath, 384);
|
|
583
|
+
await fs2.promises.chmod(certPath, 420);
|
|
502
584
|
fixOwnership(keyPath, certPath);
|
|
503
585
|
return { certPath, keyPath };
|
|
504
586
|
}
|
|
@@ -525,10 +607,10 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
|
|
|
525
607
|
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
526
608
|
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
|
|
527
609
|
try {
|
|
528
|
-
const hostCert =
|
|
610
|
+
const hostCert = fs2.readFileSync(certPath);
|
|
529
611
|
const ctx = tls.createSecureContext({
|
|
530
612
|
cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
|
|
531
|
-
key:
|
|
613
|
+
key: fs2.readFileSync(keyPath)
|
|
532
614
|
});
|
|
533
615
|
cache.set(servername, ctx);
|
|
534
616
|
cb(null, ctx);
|
|
@@ -542,8 +624,8 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
|
|
|
542
624
|
}
|
|
543
625
|
const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
|
|
544
626
|
const [hostCert, key] = await Promise.all([
|
|
545
|
-
|
|
546
|
-
|
|
627
|
+
fs2.promises.readFile(generated.certPath),
|
|
628
|
+
fs2.promises.readFile(generated.keyPath)
|
|
547
629
|
]);
|
|
548
630
|
return tls.createSecureContext({
|
|
549
631
|
cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
|
|
@@ -573,7 +655,7 @@ function trustCA(stateDir) {
|
|
|
573
655
|
if (process.platform === "darwin") {
|
|
574
656
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
575
657
|
if (isRoot) {
|
|
576
|
-
|
|
658
|
+
execFileSync2(
|
|
577
659
|
"security",
|
|
578
660
|
[
|
|
579
661
|
"add-trusted-cert",
|
|
@@ -588,7 +670,7 @@ function trustCA(stateDir) {
|
|
|
588
670
|
);
|
|
589
671
|
} else {
|
|
590
672
|
const keychain = loginKeychainPath();
|
|
591
|
-
|
|
673
|
+
execFileSync2(
|
|
592
674
|
"security",
|
|
593
675
|
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
594
676
|
{ stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
|
|
@@ -598,19 +680,19 @@ function trustCA(stateDir) {
|
|
|
598
680
|
return { trusted: true };
|
|
599
681
|
} else if (process.platform === "linux") {
|
|
600
682
|
const config = getLinuxCATrustConfig();
|
|
601
|
-
if (!
|
|
602
|
-
|
|
683
|
+
if (!fs2.existsSync(config.certDir)) {
|
|
684
|
+
fs2.mkdirSync(config.certDir, { recursive: true });
|
|
603
685
|
}
|
|
604
686
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
605
|
-
|
|
606
|
-
|
|
687
|
+
fs2.copyFileSync(caCertPath, dest);
|
|
688
|
+
execFileSync2(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
689
|
+
if (isWSL()) {
|
|
690
|
+
trustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
|
|
691
|
+
}
|
|
607
692
|
writeTrustMarker(stateDir);
|
|
608
693
|
return { trusted: true };
|
|
609
694
|
} else if (process.platform === "win32") {
|
|
610
|
-
|
|
611
|
-
stdio: "pipe",
|
|
612
|
-
timeout: 3e4
|
|
613
|
-
});
|
|
695
|
+
trustWindowsCA(caCertPath);
|
|
614
696
|
writeTrustMarker(stateDir);
|
|
615
697
|
return { trusted: true };
|
|
616
698
|
}
|
|
@@ -636,18 +718,15 @@ function untrustCA(stateDir) {
|
|
|
636
718
|
clearTrustMarker(stateDir);
|
|
637
719
|
return { removed: true };
|
|
638
720
|
}
|
|
639
|
-
|
|
640
|
-
clearTrustMarker(stateDir);
|
|
641
|
-
return { removed: true };
|
|
642
|
-
}
|
|
721
|
+
const runningInWSL = isWSL();
|
|
643
722
|
try {
|
|
644
723
|
let result;
|
|
645
724
|
if (process.platform === "darwin") {
|
|
646
725
|
result = untrustCAMacOS(caCertPath);
|
|
647
726
|
} else if (process.platform === "linux") {
|
|
648
|
-
result = untrustCALinux(stateDir);
|
|
727
|
+
result = runningInWSL ? untrustCAWSL(stateDir, caCertPath) : untrustCALinux(stateDir);
|
|
649
728
|
} else if (process.platform === "win32") {
|
|
650
|
-
result =
|
|
729
|
+
result = untrustWindowsCA(caCertPath);
|
|
651
730
|
} else {
|
|
652
731
|
result = { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
653
732
|
}
|
|
@@ -662,7 +741,7 @@ function untrustCAMacOS(caCertPath) {
|
|
|
662
741
|
const errors = [];
|
|
663
742
|
const tryExec = (args) => {
|
|
664
743
|
try {
|
|
665
|
-
|
|
744
|
+
execFileSync2("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
|
|
666
745
|
return true;
|
|
667
746
|
} catch (err) {
|
|
668
747
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -684,13 +763,13 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
|
684
763
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
685
764
|
const sudoUser = process.env.SUDO_USER;
|
|
686
765
|
if (isRoot && sudoUser) {
|
|
687
|
-
|
|
766
|
+
execFileSync2(
|
|
688
767
|
"sudo",
|
|
689
768
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
690
769
|
{ stdio: "pipe", timeout: MACOS_SECURITY_TIMEOUT_MS }
|
|
691
770
|
);
|
|
692
771
|
} else {
|
|
693
|
-
|
|
772
|
+
execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
694
773
|
stdio: "pipe",
|
|
695
774
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
696
775
|
});
|
|
@@ -700,63 +779,67 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
|
700
779
|
return false;
|
|
701
780
|
}
|
|
702
781
|
}
|
|
703
|
-
function untrustCALinux(stateDir) {
|
|
782
|
+
function untrustCALinux(stateDir, options = {}) {
|
|
704
783
|
const errors = [];
|
|
705
|
-
|
|
706
|
-
|
|
784
|
+
const pendingRefreshPath = path.join(stateDir, CA_TRUST_REFRESH_PENDING);
|
|
785
|
+
let refreshNeeded = fileExists(pendingRefreshPath);
|
|
786
|
+
const configs = options.configs ?? Object.values(LINUX_CA_TRUST_CONFIGS);
|
|
787
|
+
const activeConfig = options.activeConfig ?? getLinuxCATrustConfig();
|
|
788
|
+
const runUpdate = options.runUpdate ?? ((command) => {
|
|
789
|
+
execFileSync2(command, [], { stdio: "pipe", timeout: 3e4 });
|
|
790
|
+
});
|
|
791
|
+
for (const config of configs) {
|
|
707
792
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
708
793
|
try {
|
|
709
794
|
if (fileExists(dest)) {
|
|
710
|
-
const ours =
|
|
711
|
-
const installed =
|
|
795
|
+
const ours = fs2.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
796
|
+
const installed = fs2.readFileSync(dest, "utf-8").trim();
|
|
712
797
|
if (ours === installed) {
|
|
713
|
-
|
|
714
|
-
|
|
798
|
+
if (!refreshNeeded) {
|
|
799
|
+
fs2.writeFileSync(pendingRefreshPath, "1\n");
|
|
800
|
+
fixOwnership(pendingRefreshPath);
|
|
801
|
+
refreshNeeded = true;
|
|
802
|
+
}
|
|
803
|
+
fs2.unlinkSync(dest);
|
|
715
804
|
}
|
|
716
805
|
}
|
|
717
806
|
} catch (err) {
|
|
718
807
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
719
808
|
}
|
|
720
809
|
}
|
|
721
|
-
if (
|
|
810
|
+
if (refreshNeeded) {
|
|
722
811
|
try {
|
|
723
|
-
|
|
724
|
-
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
812
|
+
runUpdate(activeConfig.updateCommand);
|
|
725
813
|
} catch (err) {
|
|
726
814
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
727
815
|
}
|
|
728
816
|
}
|
|
729
|
-
if (
|
|
817
|
+
if (errors.length > 0 || isCATrustedLinux(stateDir, activeConfig)) {
|
|
730
818
|
return {
|
|
731
819
|
removed: false,
|
|
732
820
|
error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
|
|
733
821
|
};
|
|
734
822
|
}
|
|
823
|
+
clearTrustRefreshPending(stateDir);
|
|
735
824
|
return { removed: true };
|
|
736
825
|
}
|
|
737
|
-
function
|
|
826
|
+
function untrustCAWSL(stateDir, caCertPath) {
|
|
827
|
+
const linuxResult = untrustCALinux(stateDir);
|
|
828
|
+
let windowsResult;
|
|
738
829
|
try {
|
|
739
|
-
|
|
740
|
-
const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
741
|
-
encoding: "utf-8",
|
|
742
|
-
timeout: 1e4,
|
|
743
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
744
|
-
});
|
|
745
|
-
const normalized = storeListing.replace(/\s/g, "").toLowerCase();
|
|
746
|
-
if (!normalized.includes(fingerprint)) {
|
|
747
|
-
return { removed: true };
|
|
748
|
-
}
|
|
749
|
-
execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
|
|
750
|
-
stdio: "pipe",
|
|
751
|
-
timeout: 3e4
|
|
752
|
-
});
|
|
753
|
-
if (isCATrustedWindows(caCertPath)) {
|
|
754
|
-
return { removed: false, error: "certutil could not remove the portless CA from Root" };
|
|
755
|
-
}
|
|
756
|
-
return { removed: true };
|
|
830
|
+
windowsResult = untrustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
|
|
757
831
|
} catch (err) {
|
|
758
|
-
|
|
832
|
+
windowsResult = {
|
|
833
|
+
removed: false,
|
|
834
|
+
error: err instanceof Error ? err.message : String(err)
|
|
835
|
+
};
|
|
759
836
|
}
|
|
837
|
+
if (linuxResult.removed && windowsResult.removed) return { removed: true };
|
|
838
|
+
const errors = [linuxResult.error, windowsResult.error].filter(Boolean);
|
|
839
|
+
return {
|
|
840
|
+
removed: false,
|
|
841
|
+
error: errors.join("; ") || "Could not remove the portless CA from every WSL trust store"
|
|
842
|
+
};
|
|
760
843
|
}
|
|
761
844
|
|
|
762
845
|
// src/tailscale.ts
|
|
@@ -765,7 +848,7 @@ var TAILSCALE_BINARY = "tailscale";
|
|
|
765
848
|
var TAILSCALE_COMMAND_TIMEOUT_MS = 3e4;
|
|
766
849
|
var PREFERRED_SERVE_PORTS = [443, 8443, 8444, 8445, 8446, 8447, 8448, 8449, 8450];
|
|
767
850
|
var FUNNEL_PORTS = [443, 8443, 1e4];
|
|
768
|
-
function
|
|
851
|
+
function defaultRunner2(args) {
|
|
769
852
|
const result = spawnSync(TAILSCALE_BINARY, args, {
|
|
770
853
|
encoding: "utf-8",
|
|
771
854
|
killSignal: "SIGKILL",
|
|
@@ -857,7 +940,7 @@ function throwFunnelNotEnabled(status) {
|
|
|
857
940
|
);
|
|
858
941
|
}
|
|
859
942
|
function ensureTailscaleReady(options = {}) {
|
|
860
|
-
const runner = options.runner ??
|
|
943
|
+
const runner = options.runner ?? defaultRunner2;
|
|
861
944
|
runOrThrow(["version"], "check tailscale version", runner);
|
|
862
945
|
const statusResult = runOrThrow(["status", "--json"], "read tailscale status", runner);
|
|
863
946
|
const status = parseStatusJson(statusResult.stdout);
|
|
@@ -873,7 +956,7 @@ function ensureTailscaleReady(options = {}) {
|
|
|
873
956
|
baseUrl: `https://${dnsName}`
|
|
874
957
|
};
|
|
875
958
|
}
|
|
876
|
-
function getUsedServePorts(runner =
|
|
959
|
+
function getUsedServePorts(runner = defaultRunner2) {
|
|
877
960
|
const result = runner(["serve", "status", "--json"]);
|
|
878
961
|
if (result.error || result.status !== 0) {
|
|
879
962
|
return /* @__PURE__ */ new Set();
|
|
@@ -969,7 +1052,7 @@ function register(mode, localPort, httpsPort, runner) {
|
|
|
969
1052
|
}
|
|
970
1053
|
}
|
|
971
1054
|
function unregister(mode, httpsPort, options) {
|
|
972
|
-
const runner = options?.runner ??
|
|
1055
|
+
const runner = options?.runner ?? defaultRunner2;
|
|
973
1056
|
const result = runner([mode, "--yes", `--https=${httpsPort}`, "off"]);
|
|
974
1057
|
if (result.error) {
|
|
975
1058
|
const errno = result.error;
|
|
@@ -988,10 +1071,10 @@ ${result.stdout}`.toLowerCase();
|
|
|
988
1071
|
}
|
|
989
1072
|
}
|
|
990
1073
|
function registerServe(localPort, httpsPort, options) {
|
|
991
|
-
register("serve", localPort, httpsPort, options?.runner ??
|
|
1074
|
+
register("serve", localPort, httpsPort, options?.runner ?? defaultRunner2);
|
|
992
1075
|
}
|
|
993
1076
|
function registerFunnel(localPort, httpsPort, options) {
|
|
994
|
-
register("funnel", localPort, httpsPort, options?.runner ??
|
|
1077
|
+
register("funnel", localPort, httpsPort, options?.runner ?? defaultRunner2);
|
|
995
1078
|
}
|
|
996
1079
|
function unregisterTailscale(route) {
|
|
997
1080
|
if (!route.tailscaleHttpsPort) return;
|
|
@@ -1016,7 +1099,7 @@ function defaultSpawner(args) {
|
|
|
1016
1099
|
windowsHide: true
|
|
1017
1100
|
});
|
|
1018
1101
|
}
|
|
1019
|
-
function
|
|
1102
|
+
function defaultRunner3(args) {
|
|
1020
1103
|
const result = spawnSync2(NGROK_BINARY, args, {
|
|
1021
1104
|
encoding: "utf-8",
|
|
1022
1105
|
killSignal: "SIGKILL",
|
|
@@ -1053,7 +1136,7 @@ function formatOutputError(output) {
|
|
|
1053
1136
|
`Failed to start ngrok tunnel: ${details || "ngrok exited before printing a public URL"}`
|
|
1054
1137
|
);
|
|
1055
1138
|
}
|
|
1056
|
-
function ensureNgrokAvailable(runner =
|
|
1139
|
+
function ensureNgrokAvailable(runner = defaultRunner3) {
|
|
1057
1140
|
const result = runner(["version"]);
|
|
1058
1141
|
if (result.error) {
|
|
1059
1142
|
throw formatSpawnError(result.error);
|
|
@@ -1178,8 +1261,8 @@ function stopNgrok(route) {
|
|
|
1178
1261
|
|
|
1179
1262
|
// src/auto.ts
|
|
1180
1263
|
import { createHash as createHash2 } from "crypto";
|
|
1181
|
-
import { execFileSync as
|
|
1182
|
-
import * as
|
|
1264
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1265
|
+
import * as fs3 from "fs";
|
|
1183
1266
|
import * as path2 from "path";
|
|
1184
1267
|
var MAX_DNS_LABEL_LENGTH = 63;
|
|
1185
1268
|
function truncateLabel(label) {
|
|
@@ -1219,7 +1302,7 @@ function findPackageJsonName(startDir) {
|
|
|
1219
1302
|
for (; ; ) {
|
|
1220
1303
|
const pkgPath = path2.join(dir, "package.json");
|
|
1221
1304
|
try {
|
|
1222
|
-
const raw =
|
|
1305
|
+
const raw = fs3.readFileSync(pkgPath, "utf-8");
|
|
1223
1306
|
const pkg = JSON.parse(raw);
|
|
1224
1307
|
if (typeof pkg.name === "string" && pkg.name) {
|
|
1225
1308
|
return pkg.name.replace(/^@[^/]+\//, "");
|
|
@@ -1234,7 +1317,7 @@ function findPackageJsonName(startDir) {
|
|
|
1234
1317
|
}
|
|
1235
1318
|
function findGitRoot(startDir) {
|
|
1236
1319
|
try {
|
|
1237
|
-
const toplevel =
|
|
1320
|
+
const toplevel = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
1238
1321
|
cwd: startDir,
|
|
1239
1322
|
encoding: "utf-8",
|
|
1240
1323
|
timeout: 5e3,
|
|
@@ -1247,7 +1330,7 @@ function findGitRoot(startDir) {
|
|
|
1247
1330
|
for (; ; ) {
|
|
1248
1331
|
const gitPath = path2.join(dir, ".git");
|
|
1249
1332
|
try {
|
|
1250
|
-
const stat =
|
|
1333
|
+
const stat = fs3.statSync(gitPath);
|
|
1251
1334
|
if (stat.isDirectory()) return dir;
|
|
1252
1335
|
if (stat.isFile()) return dir;
|
|
1253
1336
|
} catch {
|
|
@@ -1258,6 +1341,9 @@ function findGitRoot(startDir) {
|
|
|
1258
1341
|
}
|
|
1259
1342
|
return null;
|
|
1260
1343
|
}
|
|
1344
|
+
function applyWorktreePrefix(baseName, worktree) {
|
|
1345
|
+
return worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
1346
|
+
}
|
|
1261
1347
|
var DEFAULT_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
|
|
1262
1348
|
function branchToPrefix(branch) {
|
|
1263
1349
|
if (!branch || branch === "HEAD" || DEFAULT_BRANCHES.has(branch)) return null;
|
|
@@ -1272,7 +1358,7 @@ function detectWorktreePrefix(cwd = process.cwd()) {
|
|
|
1272
1358
|
}
|
|
1273
1359
|
function detectWorktreeViaCli(cwd) {
|
|
1274
1360
|
try {
|
|
1275
|
-
const listOutput =
|
|
1361
|
+
const listOutput = execFileSync3("git", ["worktree", "list", "--porcelain"], {
|
|
1276
1362
|
cwd,
|
|
1277
1363
|
encoding: "utf-8",
|
|
1278
1364
|
timeout: 5e3,
|
|
@@ -1282,7 +1368,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1282
1368
|
if (worktreeCount <= 1) return null;
|
|
1283
1369
|
const gitDir = path2.resolve(
|
|
1284
1370
|
cwd,
|
|
1285
|
-
|
|
1371
|
+
execFileSync3("git", ["rev-parse", "--git-dir"], {
|
|
1286
1372
|
cwd,
|
|
1287
1373
|
encoding: "utf-8",
|
|
1288
1374
|
timeout: 5e3,
|
|
@@ -1291,7 +1377,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1291
1377
|
);
|
|
1292
1378
|
const gitCommonDir = path2.resolve(
|
|
1293
1379
|
cwd,
|
|
1294
|
-
|
|
1380
|
+
execFileSync3("git", ["rev-parse", "--git-common-dir"], {
|
|
1295
1381
|
cwd,
|
|
1296
1382
|
encoding: "utf-8",
|
|
1297
1383
|
timeout: 5e3,
|
|
@@ -1299,7 +1385,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1299
1385
|
}).trim()
|
|
1300
1386
|
);
|
|
1301
1387
|
if (gitDir === gitCommonDir) return null;
|
|
1302
|
-
const branch =
|
|
1388
|
+
const branch = execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1303
1389
|
cwd,
|
|
1304
1390
|
encoding: "utf-8",
|
|
1305
1391
|
timeout: 5e3,
|
|
@@ -1317,12 +1403,12 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
1317
1403
|
for (; ; ) {
|
|
1318
1404
|
const gitPath = path2.join(dir, ".git");
|
|
1319
1405
|
try {
|
|
1320
|
-
const stat =
|
|
1406
|
+
const stat = fs3.statSync(gitPath);
|
|
1321
1407
|
if (stat.isDirectory()) {
|
|
1322
1408
|
return null;
|
|
1323
1409
|
}
|
|
1324
1410
|
if (stat.isFile()) {
|
|
1325
|
-
const content =
|
|
1411
|
+
const content = fs3.readFileSync(gitPath, "utf-8").trim();
|
|
1326
1412
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
1327
1413
|
if (!match) return null;
|
|
1328
1414
|
const gitdir = match[1];
|
|
@@ -1342,7 +1428,7 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
1342
1428
|
}
|
|
1343
1429
|
function readBranchFromHead(gitdir) {
|
|
1344
1430
|
try {
|
|
1345
|
-
const head =
|
|
1431
|
+
const head = fs3.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
|
|
1346
1432
|
const refMatch = head.match(/^ref: refs\/heads\/(.+)$/);
|
|
1347
1433
|
return refMatch ? refMatch[1] : null;
|
|
1348
1434
|
} catch {
|
|
@@ -1351,11 +1437,11 @@ function readBranchFromHead(gitdir) {
|
|
|
1351
1437
|
}
|
|
1352
1438
|
|
|
1353
1439
|
// src/cli-utils.ts
|
|
1354
|
-
import * as
|
|
1440
|
+
import * as fs4 from "fs";
|
|
1355
1441
|
import * as http from "http";
|
|
1356
1442
|
import * as https from "https";
|
|
1357
1443
|
import * as net from "net";
|
|
1358
|
-
import * as
|
|
1444
|
+
import * as os2 from "os";
|
|
1359
1445
|
import * as path3 from "path";
|
|
1360
1446
|
import * as readline from "readline";
|
|
1361
1447
|
import { execSync, spawn as spawn2 } from "child_process";
|
|
@@ -1364,8 +1450,8 @@ var FALLBACK_PROXY_PORT = 1355;
|
|
|
1364
1450
|
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
1365
1451
|
var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
|
|
1366
1452
|
var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
|
|
1367
|
-
var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(
|
|
1368
|
-
var USER_STATE_DIR = path3.join(
|
|
1453
|
+
var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os2.tmpdir(), "portless") : "/tmp/portless";
|
|
1454
|
+
var USER_STATE_DIR = path3.join(resolveUserHome(), ".portless");
|
|
1369
1455
|
var MIN_APP_PORT = 4e3;
|
|
1370
1456
|
var MAX_APP_PORT = 4999;
|
|
1371
1457
|
var RANDOM_PORT_ATTEMPTS = 50;
|
|
@@ -1500,7 +1586,7 @@ function resolveStateDir(_port) {
|
|
|
1500
1586
|
}
|
|
1501
1587
|
function readPortFromDir(dir) {
|
|
1502
1588
|
try {
|
|
1503
|
-
const raw =
|
|
1589
|
+
const raw = fs4.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
|
|
1504
1590
|
const port = parseInt(raw, 10);
|
|
1505
1591
|
return isNaN(port) ? null : port;
|
|
1506
1592
|
} catch {
|
|
@@ -1511,7 +1597,7 @@ var TLS_MARKER_FILE = "proxy.tls";
|
|
|
1511
1597
|
var CUSTOM_CERT_MARKER_FILE = "proxy.custom-cert";
|
|
1512
1598
|
function readTlsMarker(dir) {
|
|
1513
1599
|
try {
|
|
1514
|
-
return
|
|
1600
|
+
return fs4.existsSync(path3.join(dir, TLS_MARKER_FILE));
|
|
1515
1601
|
} catch {
|
|
1516
1602
|
return false;
|
|
1517
1603
|
}
|
|
@@ -1519,17 +1605,17 @@ function readTlsMarker(dir) {
|
|
|
1519
1605
|
function writeTlsMarker(dir, enabled2) {
|
|
1520
1606
|
const markerPath = path3.join(dir, TLS_MARKER_FILE);
|
|
1521
1607
|
if (enabled2) {
|
|
1522
|
-
|
|
1608
|
+
fs4.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1523
1609
|
} else {
|
|
1524
1610
|
try {
|
|
1525
|
-
|
|
1611
|
+
fs4.unlinkSync(markerPath);
|
|
1526
1612
|
} catch {
|
|
1527
1613
|
}
|
|
1528
1614
|
}
|
|
1529
1615
|
}
|
|
1530
1616
|
function readCustomCertMarker(dir) {
|
|
1531
1617
|
try {
|
|
1532
|
-
return
|
|
1618
|
+
return fs4.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
|
|
1533
1619
|
} catch {
|
|
1534
1620
|
return false;
|
|
1535
1621
|
}
|
|
@@ -1537,10 +1623,10 @@ function readCustomCertMarker(dir) {
|
|
|
1537
1623
|
function writeCustomCertMarker(dir, enabled2) {
|
|
1538
1624
|
const markerPath = path3.join(dir, CUSTOM_CERT_MARKER_FILE);
|
|
1539
1625
|
if (enabled2) {
|
|
1540
|
-
|
|
1626
|
+
fs4.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1541
1627
|
} else {
|
|
1542
1628
|
try {
|
|
1543
|
-
|
|
1629
|
+
fs4.unlinkSync(markerPath);
|
|
1544
1630
|
} catch {
|
|
1545
1631
|
}
|
|
1546
1632
|
}
|
|
@@ -1548,7 +1634,7 @@ function writeCustomCertMarker(dir, enabled2) {
|
|
|
1548
1634
|
var LAN_MARKER_FILE = "proxy.lan";
|
|
1549
1635
|
function readLanMarker(dir) {
|
|
1550
1636
|
try {
|
|
1551
|
-
const raw =
|
|
1637
|
+
const raw = fs4.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
|
|
1552
1638
|
return raw || null;
|
|
1553
1639
|
} catch {
|
|
1554
1640
|
return null;
|
|
@@ -1558,11 +1644,11 @@ function writeLanMarker(dir, ip) {
|
|
|
1558
1644
|
const markerPath = path3.join(dir, LAN_MARKER_FILE);
|
|
1559
1645
|
if (!ip) {
|
|
1560
1646
|
try {
|
|
1561
|
-
|
|
1647
|
+
fs4.unlinkSync(markerPath);
|
|
1562
1648
|
} catch {
|
|
1563
1649
|
}
|
|
1564
1650
|
} else {
|
|
1565
|
-
|
|
1651
|
+
fs4.writeFileSync(markerPath, ip, { mode: 420 });
|
|
1566
1652
|
}
|
|
1567
1653
|
}
|
|
1568
1654
|
var DEFAULT_TLD = "localhost";
|
|
@@ -1606,7 +1692,7 @@ var TLD_FILE = "proxy.tld";
|
|
|
1606
1692
|
var TLDS_FILE = "proxy.tlds";
|
|
1607
1693
|
function readLegacyTldFromDir(dir) {
|
|
1608
1694
|
try {
|
|
1609
|
-
const raw =
|
|
1695
|
+
const raw = fs4.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
|
|
1610
1696
|
return raw || DEFAULT_TLD;
|
|
1611
1697
|
} catch {
|
|
1612
1698
|
return DEFAULT_TLD;
|
|
@@ -1614,7 +1700,7 @@ function readLegacyTldFromDir(dir) {
|
|
|
1614
1700
|
}
|
|
1615
1701
|
function readTldsFromDir(dir) {
|
|
1616
1702
|
try {
|
|
1617
|
-
const raw =
|
|
1703
|
+
const raw = fs4.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
|
|
1618
1704
|
const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
|
|
1619
1705
|
if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
|
|
1620
1706
|
const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
|
|
@@ -1629,16 +1715,16 @@ function writeTldsFile(dir, tlds) {
|
|
|
1629
1715
|
const tldPath = path3.join(dir, TLD_FILE);
|
|
1630
1716
|
if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
|
|
1631
1717
|
try {
|
|
1632
|
-
|
|
1718
|
+
fs4.unlinkSync(tldsPath);
|
|
1633
1719
|
} catch {
|
|
1634
1720
|
}
|
|
1635
1721
|
try {
|
|
1636
|
-
|
|
1722
|
+
fs4.unlinkSync(tldPath);
|
|
1637
1723
|
} catch {
|
|
1638
1724
|
}
|
|
1639
1725
|
} else {
|
|
1640
|
-
|
|
1641
|
-
|
|
1726
|
+
fs4.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
|
|
1727
|
+
fs4.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
|
|
1642
1728
|
}
|
|
1643
1729
|
}
|
|
1644
1730
|
function writeTldFile(dir, tld) {
|
|
@@ -1868,7 +1954,7 @@ function isProxyRunning(port, tls2 = false) {
|
|
|
1868
1954
|
}
|
|
1869
1955
|
function isPortListening(port) {
|
|
1870
1956
|
return new Promise((resolve4) => {
|
|
1871
|
-
const socket =
|
|
1957
|
+
const socket = createLoopbackConnection(port);
|
|
1872
1958
|
let settled = false;
|
|
1873
1959
|
const finish = (result) => {
|
|
1874
1960
|
if (settled) return;
|
|
@@ -1953,7 +2039,7 @@ function collectBinPaths(cwd) {
|
|
|
1953
2039
|
let dir = cwd;
|
|
1954
2040
|
for (; ; ) {
|
|
1955
2041
|
const bin = path3.join(dir, "node_modules", ".bin");
|
|
1956
|
-
if (
|
|
2042
|
+
if (fs4.existsSync(bin)) {
|
|
1957
2043
|
dirs.push(bin);
|
|
1958
2044
|
}
|
|
1959
2045
|
const parent = path3.dirname(dir);
|
|
@@ -2084,7 +2170,7 @@ function injectFrameworkFlags(commandArgs, port) {
|
|
|
2084
2170
|
}
|
|
2085
2171
|
|
|
2086
2172
|
// src/clean-utils.ts
|
|
2087
|
-
import * as
|
|
2173
|
+
import * as fs5 from "fs";
|
|
2088
2174
|
import * as path4 from "path";
|
|
2089
2175
|
var PORTLESS_STATE_FILES = [
|
|
2090
2176
|
"routes.json",
|
|
@@ -2097,6 +2183,8 @@ var PORTLESS_STATE_FILES = [
|
|
|
2097
2183
|
"proxy.tld",
|
|
2098
2184
|
"proxy.tlds",
|
|
2099
2185
|
"proxy.lan",
|
|
2186
|
+
"ca.trusted",
|
|
2187
|
+
"ca.trust-refresh-pending",
|
|
2100
2188
|
"ca-key.pem",
|
|
2101
2189
|
"ca.pem",
|
|
2102
2190
|
"server-key.pem",
|
|
@@ -2106,28 +2194,38 @@ var PORTLESS_STATE_FILES = [
|
|
|
2106
2194
|
"ca.srl"
|
|
2107
2195
|
];
|
|
2108
2196
|
var HOST_CERTS_DIR2 = "host-certs";
|
|
2197
|
+
var CA_IDENTITY_FILES = /* @__PURE__ */ new Set(["ca-key.pem", "ca.pem", "ca.trust-refresh-pending"]);
|
|
2109
2198
|
function collectStateDirsForCleanup() {
|
|
2110
2199
|
const dirs = /* @__PURE__ */ new Set();
|
|
2111
2200
|
const add = (d) => {
|
|
2112
2201
|
const trimmed = d?.trim();
|
|
2113
2202
|
if (!trimmed) return;
|
|
2114
2203
|
const resolved = path4.resolve(trimmed);
|
|
2115
|
-
if (
|
|
2204
|
+
if (fs5.existsSync(resolved)) dirs.add(resolved);
|
|
2116
2205
|
};
|
|
2117
2206
|
add(USER_STATE_DIR);
|
|
2118
2207
|
add(LEGACY_SYSTEM_STATE_DIR);
|
|
2119
2208
|
add(process.env.PORTLESS_STATE_DIR);
|
|
2120
2209
|
return [...dirs];
|
|
2121
2210
|
}
|
|
2122
|
-
function
|
|
2211
|
+
function attemptCATrustRemovalForCleanup(stateDirs, untrust) {
|
|
2212
|
+
const results = /* @__PURE__ */ new Map();
|
|
2213
|
+
for (const stateDir of stateDirs) {
|
|
2214
|
+
if (!fs5.existsSync(path4.join(stateDir, "ca.pem"))) continue;
|
|
2215
|
+
results.set(stateDir, untrust(stateDir));
|
|
2216
|
+
}
|
|
2217
|
+
return results;
|
|
2218
|
+
}
|
|
2219
|
+
function removePortlessStateFiles(dir, options = {}) {
|
|
2123
2220
|
for (const f of PORTLESS_STATE_FILES) {
|
|
2221
|
+
if (options.preserveCAIdentity && CA_IDENTITY_FILES.has(f)) continue;
|
|
2124
2222
|
try {
|
|
2125
|
-
|
|
2223
|
+
fs5.unlinkSync(path4.join(dir, f));
|
|
2126
2224
|
} catch {
|
|
2127
2225
|
}
|
|
2128
2226
|
}
|
|
2129
2227
|
try {
|
|
2130
|
-
|
|
2228
|
+
fs5.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
2131
2229
|
} catch {
|
|
2132
2230
|
}
|
|
2133
2231
|
}
|
|
@@ -2336,7 +2434,7 @@ function cleanupAll() {
|
|
|
2336
2434
|
}
|
|
2337
2435
|
|
|
2338
2436
|
// src/config.ts
|
|
2339
|
-
import * as
|
|
2437
|
+
import * as fs6 from "fs";
|
|
2340
2438
|
import * as path5 from "path";
|
|
2341
2439
|
var ConfigValidationError = class extends Error {
|
|
2342
2440
|
constructor(message) {
|
|
@@ -2348,7 +2446,7 @@ var CONFIG_FILENAME = "portless.json";
|
|
|
2348
2446
|
function loadConfig(cwd = process.cwd()) {
|
|
2349
2447
|
const configPath = path5.join(cwd, CONFIG_FILENAME);
|
|
2350
2448
|
try {
|
|
2351
|
-
const raw =
|
|
2449
|
+
const raw = fs6.readFileSync(configPath, "utf-8");
|
|
2352
2450
|
const parsed = JSON.parse(raw);
|
|
2353
2451
|
validateConfig(parsed, configPath);
|
|
2354
2452
|
return { config: parsed, configDir: cwd };
|
|
@@ -2371,7 +2469,7 @@ function normalizePortlessValue(value) {
|
|
|
2371
2469
|
function loadConfigFromPackageJson(dir) {
|
|
2372
2470
|
const pkgPath = path5.join(dir, "package.json");
|
|
2373
2471
|
try {
|
|
2374
|
-
const raw =
|
|
2472
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2375
2473
|
const pkg = JSON.parse(raw);
|
|
2376
2474
|
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
2377
2475
|
const config = normalizePortlessValue(pkg.portless);
|
|
@@ -2389,7 +2487,7 @@ function loadConfigFromPackageJson(dir) {
|
|
|
2389
2487
|
function loadPackagePortlessConfig(dir) {
|
|
2390
2488
|
const pkgPath = path5.join(dir, "package.json");
|
|
2391
2489
|
try {
|
|
2392
|
-
const raw =
|
|
2490
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2393
2491
|
const pkg = JSON.parse(raw);
|
|
2394
2492
|
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
2395
2493
|
const config = normalizePortlessValue(pkg.portless);
|
|
@@ -2425,7 +2523,7 @@ function resolveAppConfig(config, configDir, packageDir) {
|
|
|
2425
2523
|
function hasScript(scriptName, dir) {
|
|
2426
2524
|
const pkgPath = path5.join(dir, "package.json");
|
|
2427
2525
|
try {
|
|
2428
|
-
const raw =
|
|
2526
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2429
2527
|
const pkg = JSON.parse(raw);
|
|
2430
2528
|
return typeof pkg?.scripts?.[scriptName] === "string";
|
|
2431
2529
|
} catch {
|
|
@@ -2444,7 +2542,7 @@ function detectPackageManager(cwd) {
|
|
|
2444
2542
|
for (; ; ) {
|
|
2445
2543
|
const pkgPath = path5.join(dir, "package.json");
|
|
2446
2544
|
try {
|
|
2447
|
-
const raw =
|
|
2545
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2448
2546
|
const pkg = JSON.parse(raw);
|
|
2449
2547
|
if (typeof pkg.packageManager === "string") {
|
|
2450
2548
|
const name = pkg.packageManager.split("@")[0];
|
|
@@ -2456,7 +2554,7 @@ function detectPackageManager(cwd) {
|
|
|
2456
2554
|
}
|
|
2457
2555
|
for (const [file, pm] of LOCK_FILES) {
|
|
2458
2556
|
try {
|
|
2459
|
-
|
|
2557
|
+
fs6.accessSync(path5.join(dir, file), fs6.constants.F_OK);
|
|
2460
2558
|
return pm;
|
|
2461
2559
|
} catch {
|
|
2462
2560
|
}
|
|
@@ -2615,13 +2713,13 @@ function warnUnknownKeys(obj, known, configPath, prefix) {
|
|
|
2615
2713
|
}
|
|
2616
2714
|
|
|
2617
2715
|
// src/workspace.ts
|
|
2618
|
-
import * as
|
|
2716
|
+
import * as fs7 from "fs";
|
|
2619
2717
|
import * as path6 from "path";
|
|
2620
2718
|
function findWorkspaceRoot(cwd = process.cwd()) {
|
|
2621
2719
|
let dir = cwd;
|
|
2622
2720
|
for (; ; ) {
|
|
2623
2721
|
try {
|
|
2624
|
-
|
|
2722
|
+
fs7.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs7.constants.R_OK);
|
|
2625
2723
|
return dir;
|
|
2626
2724
|
} catch {
|
|
2627
2725
|
}
|
|
@@ -2635,7 +2733,7 @@ function findWorkspaceRoot(cwd = process.cwd()) {
|
|
|
2635
2733
|
}
|
|
2636
2734
|
function detectWorkspaceSource(workspaceRoot) {
|
|
2637
2735
|
try {
|
|
2638
|
-
|
|
2736
|
+
fs7.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs7.constants.R_OK);
|
|
2639
2737
|
return "pnpm";
|
|
2640
2738
|
} catch {
|
|
2641
2739
|
}
|
|
@@ -2647,7 +2745,7 @@ function detectWorkspaceSource(workspaceRoot) {
|
|
|
2647
2745
|
function readWorkspacesFromPackageJson(dir) {
|
|
2648
2746
|
const pkgPath = path6.join(dir, "package.json");
|
|
2649
2747
|
try {
|
|
2650
|
-
const raw =
|
|
2748
|
+
const raw = fs7.readFileSync(pkgPath, "utf-8");
|
|
2651
2749
|
const pkg = JSON.parse(raw);
|
|
2652
2750
|
if (!pkg || typeof pkg !== "object") return null;
|
|
2653
2751
|
const ws = pkg.workspaces;
|
|
@@ -2668,7 +2766,7 @@ function discoverWorkspacePackages(workspaceRoot) {
|
|
|
2668
2766
|
const wsPath = path6.join(workspaceRoot, "pnpm-workspace.yaml");
|
|
2669
2767
|
let content;
|
|
2670
2768
|
try {
|
|
2671
|
-
content =
|
|
2769
|
+
content = fs7.readFileSync(wsPath, "utf-8");
|
|
2672
2770
|
} catch {
|
|
2673
2771
|
return [];
|
|
2674
2772
|
}
|
|
@@ -2683,7 +2781,7 @@ function discoverWorkspacePackages(workspaceRoot) {
|
|
|
2683
2781
|
for (const dir of dirs) {
|
|
2684
2782
|
const pkgPath = path6.join(dir, "package.json");
|
|
2685
2783
|
try {
|
|
2686
|
-
const raw =
|
|
2784
|
+
const raw = fs7.readFileSync(pkgPath, "utf-8");
|
|
2687
2785
|
const pkg = JSON.parse(raw);
|
|
2688
2786
|
const rawName = typeof pkg.name === "string" ? pkg.name : null;
|
|
2689
2787
|
const scopeMatch = rawName?.match(/^@([^/]+)\//);
|
|
@@ -2765,7 +2863,7 @@ function expandSingleGlob(root, glob) {
|
|
|
2765
2863
|
function expandSegments(base, segments) {
|
|
2766
2864
|
if (segments.length === 0) {
|
|
2767
2865
|
try {
|
|
2768
|
-
const stat =
|
|
2866
|
+
const stat = fs7.statSync(base);
|
|
2769
2867
|
if (stat.isDirectory()) return [base];
|
|
2770
2868
|
} catch {
|
|
2771
2869
|
}
|
|
@@ -2774,7 +2872,7 @@ function expandSegments(base, segments) {
|
|
|
2774
2872
|
const [current, ...rest] = segments;
|
|
2775
2873
|
if (current.includes("*")) {
|
|
2776
2874
|
try {
|
|
2777
|
-
const entries =
|
|
2875
|
+
const entries = fs7.readdirSync(base, { withFileTypes: true });
|
|
2778
2876
|
const matched = entries.filter((e) => e.isDirectory() && segmentMatches(current, e.name));
|
|
2779
2877
|
if (rest.length === 0) {
|
|
2780
2878
|
return matched.map((e) => path6.join(base, e.name));
|
|
@@ -2792,7 +2890,7 @@ function expandSegments(base, segments) {
|
|
|
2792
2890
|
}
|
|
2793
2891
|
|
|
2794
2892
|
// src/turbo.ts
|
|
2795
|
-
import * as
|
|
2893
|
+
import * as fs8 from "fs";
|
|
2796
2894
|
import * as path7 from "path";
|
|
2797
2895
|
var LOADER_FILENAME = "turbo-env-loader.cjs";
|
|
2798
2896
|
var MANIFEST_FILENAME = "dev-manifest.json";
|
|
@@ -2822,23 +2920,23 @@ try {
|
|
|
2822
2920
|
`;
|
|
2823
2921
|
}
|
|
2824
2922
|
function ensureEnvLoader(baseDir = USER_STATE_DIR) {
|
|
2825
|
-
|
|
2923
|
+
fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2826
2924
|
const target = loaderPath(baseDir);
|
|
2827
2925
|
const source = loaderSource(baseDir);
|
|
2828
2926
|
try {
|
|
2829
|
-
const existing =
|
|
2927
|
+
const existing = fs8.readFileSync(target, "utf-8");
|
|
2830
2928
|
if (existing === source) return;
|
|
2831
2929
|
} catch {
|
|
2832
2930
|
}
|
|
2833
|
-
|
|
2931
|
+
fs8.writeFileSync(target, source, { mode: 420 });
|
|
2834
2932
|
}
|
|
2835
2933
|
function writeManifest(entries, baseDir = USER_STATE_DIR) {
|
|
2836
|
-
|
|
2837
|
-
|
|
2934
|
+
fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2935
|
+
fs8.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
|
|
2838
2936
|
}
|
|
2839
2937
|
function removeManifest(baseDir = USER_STATE_DIR) {
|
|
2840
2938
|
try {
|
|
2841
|
-
|
|
2939
|
+
fs8.unlinkSync(manifestPath(baseDir));
|
|
2842
2940
|
} catch {
|
|
2843
2941
|
}
|
|
2844
2942
|
}
|
|
@@ -2850,7 +2948,7 @@ function buildNodeOptions(baseDir = USER_STATE_DIR) {
|
|
|
2850
2948
|
}
|
|
2851
2949
|
function hasTurboConfig(wsRoot) {
|
|
2852
2950
|
try {
|
|
2853
|
-
|
|
2951
|
+
fs8.accessSync(path7.join(wsRoot, "turbo.json"), fs8.constants.R_OK);
|
|
2854
2952
|
return true;
|
|
2855
2953
|
} catch {
|
|
2856
2954
|
return false;
|
|
@@ -2858,8 +2956,8 @@ function hasTurboConfig(wsRoot) {
|
|
|
2858
2956
|
}
|
|
2859
2957
|
|
|
2860
2958
|
// src/service.ts
|
|
2861
|
-
import * as
|
|
2862
|
-
import * as
|
|
2959
|
+
import * as fs9 from "fs";
|
|
2960
|
+
import * as os3 from "os";
|
|
2863
2961
|
import * as path8 from "path";
|
|
2864
2962
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2865
2963
|
var DEFAULT_SERVICE_PORT = getProtocolPort(true);
|
|
@@ -2890,7 +2988,7 @@ var DEFAULT_SERVICE_CONFIG = {
|
|
|
2890
2988
|
useWildcard: false,
|
|
2891
2989
|
extraEnv: {}
|
|
2892
2990
|
};
|
|
2893
|
-
function
|
|
2991
|
+
function defaultRunner4(command, args, options) {
|
|
2894
2992
|
return spawnSync4(command, args, {
|
|
2895
2993
|
encoding: "utf-8",
|
|
2896
2994
|
stdio: options?.stdio ?? "pipe"
|
|
@@ -2932,7 +3030,7 @@ function getFlagValue(args, index, flag) {
|
|
|
2932
3030
|
return value;
|
|
2933
3031
|
}
|
|
2934
3032
|
function resolveServicePath(value) {
|
|
2935
|
-
const expanded = value === "~" ?
|
|
3033
|
+
const expanded = value === "~" ? os3.homedir() : value.startsWith("~/") || value.startsWith("~\\") ? path8.join(os3.homedir(), value.slice(2)) : value;
|
|
2936
3034
|
return path8.resolve(expanded);
|
|
2937
3035
|
}
|
|
2938
3036
|
function normalizeServiceInstallPaths(config) {
|
|
@@ -3060,34 +3158,21 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
3060
3158
|
}
|
|
3061
3159
|
return config;
|
|
3062
3160
|
}
|
|
3063
|
-
function readPasswdHome(username) {
|
|
3064
|
-
try {
|
|
3065
|
-
const passwd = fs8.readFileSync("/etc/passwd", "utf-8");
|
|
3066
|
-
for (const line of passwd.split("\n")) {
|
|
3067
|
-
const fields = line.split(":");
|
|
3068
|
-
if (fields[0] === username && fields[5]) {
|
|
3069
|
-
return fields[5];
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
} catch {
|
|
3073
|
-
}
|
|
3074
|
-
return null;
|
|
3075
|
-
}
|
|
3076
3161
|
function resolveUserContext(platform) {
|
|
3077
3162
|
if (platform === "win32") {
|
|
3078
|
-
const home =
|
|
3163
|
+
const home = resolveUserHome({ platform });
|
|
3079
3164
|
return { home, username: process.env.USERNAME };
|
|
3080
3165
|
}
|
|
3081
3166
|
const sudoUser = process.env.SUDO_USER;
|
|
3082
3167
|
const sudoUid = process.env.SUDO_UID;
|
|
3083
3168
|
const sudoGid = process.env.SUDO_GID;
|
|
3084
3169
|
if (sudoUser && sudoUser !== "root") {
|
|
3085
|
-
const home =
|
|
3170
|
+
const home = resolveUserHome({ platform });
|
|
3086
3171
|
return { home, uid: sudoUid, gid: sudoGid, username: sudoUser };
|
|
3087
3172
|
}
|
|
3088
|
-
const userInfo2 =
|
|
3173
|
+
const userInfo2 = os3.userInfo();
|
|
3089
3174
|
return {
|
|
3090
|
-
home:
|
|
3175
|
+
home: os3.homedir(),
|
|
3091
3176
|
uid: process.getuid?.()?.toString(),
|
|
3092
3177
|
gid: process.getgid?.()?.toString(),
|
|
3093
3178
|
username: userInfo2.username
|
|
@@ -3365,8 +3450,8 @@ function parsePlistEnv(block) {
|
|
|
3365
3450
|
function readInstalledServiceSnapshot(spec) {
|
|
3366
3451
|
try {
|
|
3367
3452
|
if (spec.platform === "darwin") {
|
|
3368
|
-
if (!
|
|
3369
|
-
const plist =
|
|
3453
|
+
if (!fs9.existsSync(spec.plistPath)) return null;
|
|
3454
|
+
const plist = fs9.readFileSync(spec.plistPath, "utf-8");
|
|
3370
3455
|
const argsBlock = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
3371
3456
|
const envBlock = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
|
|
3372
3457
|
if (!argsBlock) return null;
|
|
@@ -3376,8 +3461,8 @@ function readInstalledServiceSnapshot(spec) {
|
|
|
3376
3461
|
};
|
|
3377
3462
|
}
|
|
3378
3463
|
if (spec.platform === "linux") {
|
|
3379
|
-
if (!
|
|
3380
|
-
const unit =
|
|
3464
|
+
if (!fs9.existsSync(spec.unitPath)) return null;
|
|
3465
|
+
const unit = fs9.readFileSync(spec.unitPath, "utf-8");
|
|
3381
3466
|
const env2 = {};
|
|
3382
3467
|
let command = null;
|
|
3383
3468
|
for (const line of unit.split("\n")) {
|
|
@@ -3395,8 +3480,8 @@ function readInstalledServiceSnapshot(spec) {
|
|
|
3395
3480
|
}
|
|
3396
3481
|
return command ? { command, env: env2 } : null;
|
|
3397
3482
|
}
|
|
3398
|
-
if (!
|
|
3399
|
-
const script =
|
|
3483
|
+
if (!fs9.existsSync(spec.scriptPath)) return null;
|
|
3484
|
+
const script = fs9.readFileSync(spec.scriptPath, "utf-8");
|
|
3400
3485
|
const env = {};
|
|
3401
3486
|
let commandLine = null;
|
|
3402
3487
|
for (const rawLine of script.split(/\r?\n/)) {
|
|
@@ -3458,7 +3543,7 @@ function buildElevatedEnvArgs(options) {
|
|
|
3458
3543
|
}
|
|
3459
3544
|
function buildServiceUninstallSudoArgs(entryScript, options = {}) {
|
|
3460
3545
|
const env = options.env ?? process.env;
|
|
3461
|
-
const home = options.home ??
|
|
3546
|
+
const home = options.home ?? os3.homedir();
|
|
3462
3547
|
const stateDir = options.stateDir ?? env.PORTLESS_STATE_DIR ?? path8.join(home, ".portless");
|
|
3463
3548
|
return [
|
|
3464
3549
|
...buildElevatedEnvArgs({ home, stateDir, env }),
|
|
@@ -3472,7 +3557,7 @@ function requireUnixElevation(args, runner) {
|
|
|
3472
3557
|
if (process.platform !== "darwin" && process.platform !== "linux") return;
|
|
3473
3558
|
if ((process.getuid?.() ?? -1) === 0) return;
|
|
3474
3559
|
if (process.env[INTERNAL_ELEVATED_ENV] === "1") return;
|
|
3475
|
-
const home =
|
|
3560
|
+
const home = os3.homedir();
|
|
3476
3561
|
const stateDir = process.env.PORTLESS_STATE_DIR || path8.join(home, ".portless");
|
|
3477
3562
|
const result = runner(
|
|
3478
3563
|
"sudo",
|
|
@@ -3529,7 +3614,7 @@ async function stopExistingProxy(entryScript, runner, proxyPort) {
|
|
|
3529
3614
|
}
|
|
3530
3615
|
}
|
|
3531
3616
|
function prepareServiceState(stateDir) {
|
|
3532
|
-
|
|
3617
|
+
fs9.mkdirSync(stateDir, { recursive: true });
|
|
3533
3618
|
fixOwnership(stateDir);
|
|
3534
3619
|
}
|
|
3535
3620
|
function prepareTrust(stateDir) {
|
|
@@ -3577,8 +3662,8 @@ async function installService(entryScript, runner, args) {
|
|
|
3577
3662
|
if (spec.platform === "darwin") {
|
|
3578
3663
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3579
3664
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3580
|
-
|
|
3581
|
-
|
|
3665
|
+
fs9.writeFileSync(spec.plistPath, spec.plist);
|
|
3666
|
+
fs9.chmodSync(spec.plistPath, 420);
|
|
3582
3667
|
runRequired(runner, "chown", ["root:wheel", spec.plistPath]);
|
|
3583
3668
|
runRequired(runner, "launchctl", ["bootstrap", "system", spec.plistPath]);
|
|
3584
3669
|
runRequired(runner, "launchctl", ["enable", `system/${spec.label}`]);
|
|
@@ -3586,16 +3671,16 @@ async function installService(entryScript, runner, args) {
|
|
|
3586
3671
|
} else if (spec.platform === "linux") {
|
|
3587
3672
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3588
3673
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3589
|
-
|
|
3590
|
-
|
|
3674
|
+
fs9.writeFileSync(spec.unitPath, spec.unit);
|
|
3675
|
+
fs9.chmodSync(spec.unitPath, 420);
|
|
3591
3676
|
runRequired(runner, "systemctl", ["daemon-reload"]);
|
|
3592
3677
|
runRequired(runner, "systemctl", ["enable", spec.serviceName]);
|
|
3593
3678
|
runRequired(runner, "systemctl", ["restart", spec.serviceName]);
|
|
3594
3679
|
} else {
|
|
3595
3680
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3596
3681
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3597
|
-
|
|
3598
|
-
|
|
3682
|
+
fs9.mkdirSync(spec.scriptDir, { recursive: true });
|
|
3683
|
+
fs9.writeFileSync(spec.scriptPath, spec.script);
|
|
3599
3684
|
runRequired(runner, "schtasks", spec.createArgs);
|
|
3600
3685
|
runOptional(runner, "schtasks", spec.runArgs);
|
|
3601
3686
|
}
|
|
@@ -3608,32 +3693,32 @@ async function uninstallService(entryScript, runner) {
|
|
|
3608
3693
|
const spec = currentServiceSpec(entryScript);
|
|
3609
3694
|
if (spec.platform === "darwin") {
|
|
3610
3695
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3611
|
-
|
|
3696
|
+
fs9.rmSync(spec.plistPath, { force: true });
|
|
3612
3697
|
} else if (spec.platform === "linux") {
|
|
3613
3698
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3614
|
-
|
|
3699
|
+
fs9.rmSync(spec.unitPath, { force: true });
|
|
3615
3700
|
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3616
3701
|
} else {
|
|
3617
3702
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3618
3703
|
runOptional(runner, "schtasks", spec.deleteArgs);
|
|
3619
|
-
|
|
3704
|
+
fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3620
3705
|
}
|
|
3621
3706
|
console.log(colors_default.green("Portless service uninstalled."));
|
|
3622
3707
|
}
|
|
3623
|
-
function tryUninstallService(entryScript, runner =
|
|
3708
|
+
function tryUninstallService(entryScript, runner = defaultRunner4) {
|
|
3624
3709
|
let installed = false;
|
|
3625
3710
|
try {
|
|
3626
3711
|
const spec = currentServiceSpec(entryScript);
|
|
3627
3712
|
if (spec.platform === "darwin") {
|
|
3628
|
-
installed =
|
|
3713
|
+
installed = fs9.existsSync(spec.plistPath);
|
|
3629
3714
|
if (!installed) return { removed: false, installed: false };
|
|
3630
3715
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3631
|
-
|
|
3716
|
+
fs9.rmSync(spec.plistPath, { force: true });
|
|
3632
3717
|
} else if (spec.platform === "linux") {
|
|
3633
|
-
installed =
|
|
3718
|
+
installed = fs9.existsSync(spec.unitPath);
|
|
3634
3719
|
if (!installed) return { removed: false, installed: false };
|
|
3635
3720
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3636
|
-
|
|
3721
|
+
fs9.rmSync(spec.unitPath, { force: true });
|
|
3637
3722
|
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3638
3723
|
} else {
|
|
3639
3724
|
const query = runner("schtasks", ["/Query", "/TN", spec.taskName, "/FO", "LIST"]);
|
|
@@ -3641,7 +3726,7 @@ function tryUninstallService(entryScript, runner = defaultRunner3) {
|
|
|
3641
3726
|
if (!installed) return { removed: false, installed: false };
|
|
3642
3727
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3643
3728
|
runRequired(runner, "schtasks", spec.deleteArgs);
|
|
3644
|
-
|
|
3729
|
+
fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3645
3730
|
}
|
|
3646
3731
|
return { removed: true, installed: true };
|
|
3647
3732
|
} catch (err) {
|
|
@@ -3658,7 +3743,7 @@ async function getServiceStatus(entryScript, runner) {
|
|
|
3658
3743
|
const installedConfig = readInstalledServiceConfig(spec) ?? spec.config;
|
|
3659
3744
|
const proxyRunning = await isProxyRunning(installedConfig.proxyPort, installedConfig.useHttps);
|
|
3660
3745
|
if (spec.platform === "darwin") {
|
|
3661
|
-
const installed2 =
|
|
3746
|
+
const installed2 = fs9.existsSync(spec.plistPath);
|
|
3662
3747
|
const result = runner("launchctl", ["print", `system/${spec.label}`]);
|
|
3663
3748
|
const output2 = `${result.stdout || ""}${result.stderr || ""}`;
|
|
3664
3749
|
const managerState = result.status === 0 && /state = running|pid = \d+/.test(output2) ? "running" : installed2 ? "installed" : "not installed";
|
|
@@ -3673,7 +3758,7 @@ async function getServiceStatus(entryScript, runner) {
|
|
|
3673
3758
|
if (spec.platform === "linux") {
|
|
3674
3759
|
const enabled2 = runner("systemctl", ["is-enabled", spec.serviceName]);
|
|
3675
3760
|
const active = runner("systemctl", ["is-active", spec.serviceName]);
|
|
3676
|
-
const installed2 = enabled2.status === 0 || active.status === 0 ||
|
|
3761
|
+
const installed2 = enabled2.status === 0 || active.status === 0 || fs9.existsSync(spec.unitPath);
|
|
3677
3762
|
const activeText = (active.stdout || "").trim();
|
|
3678
3763
|
return {
|
|
3679
3764
|
installed: installed2,
|
|
@@ -3748,7 +3833,7 @@ ${colors_default.bold("Notes:")}
|
|
|
3748
3833
|
}
|
|
3749
3834
|
async function handleService(args, options) {
|
|
3750
3835
|
const action = args[1];
|
|
3751
|
-
const runner = options.runner ||
|
|
3836
|
+
const runner = options.runner || defaultRunner4;
|
|
3752
3837
|
if (!action || action === "--help" || action === "-h") {
|
|
3753
3838
|
printServiceHelp();
|
|
3754
3839
|
process.exit(0);
|
|
@@ -3945,7 +4030,7 @@ function getEntryScript() {
|
|
|
3945
4030
|
function isLocallyInstalled() {
|
|
3946
4031
|
let dir = process.cwd();
|
|
3947
4032
|
for (; ; ) {
|
|
3948
|
-
if (
|
|
4033
|
+
if (fs10.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
|
|
3949
4034
|
return true;
|
|
3950
4035
|
}
|
|
3951
4036
|
const parent = path9.dirname(dir);
|
|
@@ -4059,11 +4144,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4059
4144
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
4060
4145
|
}
|
|
4061
4146
|
const routesPath = store.getRoutesPath();
|
|
4062
|
-
if (!
|
|
4063
|
-
|
|
4147
|
+
if (!fs10.existsSync(routesPath)) {
|
|
4148
|
+
fs10.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
4064
4149
|
}
|
|
4065
4150
|
try {
|
|
4066
|
-
|
|
4151
|
+
fs10.chmodSync(routesPath, FILE_MODE);
|
|
4067
4152
|
} catch {
|
|
4068
4153
|
}
|
|
4069
4154
|
fixOwnership(routesPath);
|
|
@@ -4123,7 +4208,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4123
4208
|
}
|
|
4124
4209
|
};
|
|
4125
4210
|
try {
|
|
4126
|
-
watcher =
|
|
4211
|
+
watcher = fs10.watch(routesPath, () => {
|
|
4127
4212
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
4128
4213
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
4129
4214
|
});
|
|
@@ -4174,8 +4259,8 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4174
4259
|
redirectServer.listen(80);
|
|
4175
4260
|
}
|
|
4176
4261
|
server.listen(proxyPort, () => {
|
|
4177
|
-
|
|
4178
|
-
|
|
4262
|
+
fs10.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
4263
|
+
fs10.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
4179
4264
|
writeTlsMarker(store.dir, isTls);
|
|
4180
4265
|
writeCustomCertMarker(store.dir, isTls && customCert);
|
|
4181
4266
|
writeTldsFile(store.dir, tlds);
|
|
@@ -4224,11 +4309,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4224
4309
|
redirectServer.close();
|
|
4225
4310
|
}
|
|
4226
4311
|
try {
|
|
4227
|
-
|
|
4312
|
+
fs10.unlinkSync(store.pidPath);
|
|
4228
4313
|
} catch {
|
|
4229
4314
|
}
|
|
4230
4315
|
try {
|
|
4231
|
-
|
|
4316
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4232
4317
|
} catch {
|
|
4233
4318
|
}
|
|
4234
4319
|
writeTlsMarker(store.dir, false);
|
|
@@ -4259,7 +4344,7 @@ function sudoStopOrHint(port) {
|
|
|
4259
4344
|
}
|
|
4260
4345
|
async function stopProxy(store, proxyPort, _tls) {
|
|
4261
4346
|
const pidPath = store.pidPath;
|
|
4262
|
-
if (!
|
|
4347
|
+
if (!fs10.existsSync(pidPath)) {
|
|
4263
4348
|
if (await isProxyRunning(proxyPort)) {
|
|
4264
4349
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
4265
4350
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -4267,7 +4352,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4267
4352
|
try {
|
|
4268
4353
|
process.kill(pid, "SIGTERM");
|
|
4269
4354
|
try {
|
|
4270
|
-
|
|
4355
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4271
4356
|
} catch {
|
|
4272
4357
|
}
|
|
4273
4358
|
writeTlsMarker(store.dir, false);
|
|
@@ -4306,10 +4391,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4306
4391
|
return;
|
|
4307
4392
|
}
|
|
4308
4393
|
try {
|
|
4309
|
-
const pid = parseInt(
|
|
4394
|
+
const pid = parseInt(fs10.readFileSync(pidPath, "utf-8"), 10);
|
|
4310
4395
|
if (isNaN(pid)) {
|
|
4311
4396
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
4312
|
-
|
|
4397
|
+
fs10.unlinkSync(pidPath);
|
|
4313
4398
|
writeTlsMarker(store.dir, false);
|
|
4314
4399
|
writeCustomCertMarker(store.dir, false);
|
|
4315
4400
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
@@ -4324,9 +4409,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4324
4409
|
return;
|
|
4325
4410
|
}
|
|
4326
4411
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
4327
|
-
|
|
4412
|
+
fs10.unlinkSync(pidPath);
|
|
4328
4413
|
try {
|
|
4329
|
-
|
|
4414
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4330
4415
|
} catch {
|
|
4331
4416
|
}
|
|
4332
4417
|
writeTlsMarker(store.dir, false);
|
|
@@ -4342,7 +4427,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4342
4427
|
)
|
|
4343
4428
|
);
|
|
4344
4429
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
4345
|
-
|
|
4430
|
+
fs10.unlinkSync(pidPath);
|
|
4346
4431
|
writeTlsMarker(store.dir, false);
|
|
4347
4432
|
writeCustomCertMarker(store.dir, false);
|
|
4348
4433
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
@@ -4350,9 +4435,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4350
4435
|
return;
|
|
4351
4436
|
}
|
|
4352
4437
|
process.kill(pid, "SIGTERM");
|
|
4353
|
-
|
|
4438
|
+
fs10.unlinkSync(pidPath);
|
|
4354
4439
|
try {
|
|
4355
|
-
|
|
4440
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4356
4441
|
} catch {
|
|
4357
4442
|
}
|
|
4358
4443
|
writeTlsMarker(store.dir, false);
|
|
@@ -4503,7 +4588,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4503
4588
|
const logPath = path9.join(fallbackDir, "proxy.log");
|
|
4504
4589
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
4505
4590
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
4506
|
-
if (
|
|
4591
|
+
if (fs10.existsSync(logPath)) {
|
|
4507
4592
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
4508
4593
|
}
|
|
4509
4594
|
process.exit(1);
|
|
@@ -4768,7 +4853,7 @@ portless
|
|
|
4768
4853
|
const caEnv = {};
|
|
4769
4854
|
if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
|
|
4770
4855
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
4771
|
-
if (
|
|
4856
|
+
if (fs10.existsSync(caPath)) {
|
|
4772
4857
|
caEnv.NODE_EXTRA_CA_CERTS = caPath;
|
|
4773
4858
|
}
|
|
4774
4859
|
}
|
|
@@ -5047,11 +5132,13 @@ ${colors_default.bold("How it works:")}
|
|
|
5047
5132
|
5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
|
|
5048
5133
|
Expo, React Native) get --port and, when needed, --host flags
|
|
5049
5134
|
injected automatically
|
|
5135
|
+
Elevated proxy processes keep the invoking user's ~/.portless state directory.
|
|
5050
5136
|
|
|
5051
5137
|
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
5052
5138
|
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
5053
5139
|
On first use, portless generates a local CA and adds it to your
|
|
5054
5140
|
system trust store. No browser warnings. Disable with --no-tls.
|
|
5141
|
+
On WSL, portless also adds the CA to the Windows user certificate store.
|
|
5055
5142
|
|
|
5056
5143
|
${colors_default.bold("LAN mode:")}
|
|
5057
5144
|
Use --lan to make services accessible from other devices (phones,
|
|
@@ -5153,13 +5240,13 @@ ${colors_default.bold("Reserved names:")}
|
|
|
5153
5240
|
process.exit(0);
|
|
5154
5241
|
}
|
|
5155
5242
|
function printVersion() {
|
|
5156
|
-
console.log("0.15.
|
|
5243
|
+
console.log("0.15.3");
|
|
5157
5244
|
process.exit(0);
|
|
5158
5245
|
}
|
|
5159
5246
|
async function handleTrust() {
|
|
5160
5247
|
const { dir } = await discoverState();
|
|
5161
|
-
if (!
|
|
5162
|
-
|
|
5248
|
+
if (!fs10.existsSync(dir)) {
|
|
5249
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
5163
5250
|
}
|
|
5164
5251
|
const { caGenerated } = ensureCerts(dir);
|
|
5165
5252
|
if (caGenerated) {
|
|
@@ -5207,7 +5294,8 @@ directory, and PORTLESS_STATE_DIR when set), and removes the portless block
|
|
|
5207
5294
|
from ${HOSTS_DISPLAY}.
|
|
5208
5295
|
|
|
5209
5296
|
Only allowlisted filenames under each state directory are deleted. Custom
|
|
5210
|
-
certificate paths from --cert and --key are never removed.
|
|
5297
|
+
certificate paths from --cert and --key are never removed. If trust removal
|
|
5298
|
+
fails, the CA certificate and key are retained so clean can safely retry.
|
|
5211
5299
|
|
|
5212
5300
|
macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
|
|
5213
5301
|
require elevated privileges. On Windows, run as Administrator if needed.
|
|
@@ -5263,27 +5351,32 @@ ${colors_default.bold("Options:")}
|
|
|
5263
5351
|
}
|
|
5264
5352
|
}
|
|
5265
5353
|
const stateDirs = collectStateDirsForCleanup();
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
const wasTrusted = isCATrusted(stateDir);
|
|
5270
|
-
if (!wasTrusted) continue;
|
|
5271
|
-
const untrustResult = untrustCA(stateDir);
|
|
5354
|
+
const failedCAStateDirs = /* @__PURE__ */ new Set();
|
|
5355
|
+
const caRemovalResults = attemptCATrustRemovalForCleanup(stateDirs, untrustCA);
|
|
5356
|
+
for (const [stateDir, untrustResult] of caRemovalResults) {
|
|
5272
5357
|
if (untrustResult.removed) {
|
|
5273
5358
|
console.log(colors_default.green("Removed local CA from the system trust store."));
|
|
5274
|
-
} else
|
|
5359
|
+
} else {
|
|
5360
|
+
failedCAStateDirs.add(stateDir);
|
|
5275
5361
|
console.warn(
|
|
5276
5362
|
colors_default.yellow(
|
|
5277
|
-
`Could not remove CA from trust store: ${untrustResult.error}
|
|
5363
|
+
`Could not remove CA from trust store: ${untrustResult.error ?? "unknown error"}
|
|
5278
5364
|
Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
5279
5365
|
)
|
|
5280
5366
|
);
|
|
5281
5367
|
}
|
|
5282
5368
|
}
|
|
5283
5369
|
for (const stateDir of stateDirs) {
|
|
5284
|
-
removePortlessStateFiles(stateDir
|
|
5370
|
+
removePortlessStateFiles(stateDir, {
|
|
5371
|
+
preserveCAIdentity: failedCAStateDirs.has(stateDir)
|
|
5372
|
+
});
|
|
5285
5373
|
}
|
|
5286
5374
|
console.log(colors_default.green("Removed portless state files from known state directories."));
|
|
5375
|
+
if (failedCAStateDirs.size > 0) {
|
|
5376
|
+
console.warn(
|
|
5377
|
+
colors_default.yellow("Retained CA identity files so trust removal can be retried safely.")
|
|
5378
|
+
);
|
|
5379
|
+
}
|
|
5287
5380
|
if (cleanHostsFile()) {
|
|
5288
5381
|
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
5289
5382
|
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
@@ -5609,7 +5702,7 @@ function isProcessAliveForDoctor(pid) {
|
|
|
5609
5702
|
}
|
|
5610
5703
|
function checkPathWritable(targetPath) {
|
|
5611
5704
|
try {
|
|
5612
|
-
|
|
5705
|
+
fs10.accessSync(targetPath, fs10.constants.W_OK);
|
|
5613
5706
|
return true;
|
|
5614
5707
|
} catch {
|
|
5615
5708
|
return false;
|
|
@@ -5618,7 +5711,7 @@ function checkPathWritable(targetPath) {
|
|
|
5618
5711
|
function findExistingAncestor(targetPath) {
|
|
5619
5712
|
let current = targetPath;
|
|
5620
5713
|
for (; ; ) {
|
|
5621
|
-
if (
|
|
5714
|
+
if (fs10.existsSync(current)) return current;
|
|
5622
5715
|
const parent = path9.dirname(current);
|
|
5623
5716
|
if (parent === current) return null;
|
|
5624
5717
|
current = parent;
|
|
@@ -5689,7 +5782,7 @@ ${colors_default.bold("Options:")}
|
|
|
5689
5782
|
const store = new RouteStore(state.dir, {
|
|
5690
5783
|
onWarning: (msg) => add("warn", msg)
|
|
5691
5784
|
});
|
|
5692
|
-
const hasPortFile =
|
|
5785
|
+
const hasPortFile = fs10.existsSync(store.portFilePath);
|
|
5693
5786
|
const configuredTls = hasPortFile ? state.tls : !isHttpsEnvDisabled();
|
|
5694
5787
|
const configuredPort = hasPortFile ? state.port : getDefaultPort(configuredTls);
|
|
5695
5788
|
const initialProxyRunning = await isProxyRunning(state.port, state.tls);
|
|
@@ -5701,9 +5794,9 @@ ${colors_default.bold("Options:")}
|
|
|
5701
5794
|
const proxyTls = proxyRunning || portListening || hasPortFile ? probeTls : configuredTls;
|
|
5702
5795
|
const currentProxyStateIsHttp = (proxyRunning || portListening || hasPortFile) && !proxyTls;
|
|
5703
5796
|
const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
|
|
5704
|
-
const stateExists =
|
|
5797
|
+
const stateExists = fs10.existsSync(state.dir);
|
|
5705
5798
|
console.log(colors_default.blue.bold("\nportless doctor\n"));
|
|
5706
|
-
console.log(`Version: ${"0.15.
|
|
5799
|
+
console.log(`Version: ${"0.15.3"}`);
|
|
5707
5800
|
console.log(`Node.js: ${process.versions.node}`);
|
|
5708
5801
|
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
5709
5802
|
console.log(`State dir: ${state.dir}`);
|
|
@@ -5720,7 +5813,7 @@ ${colors_default.bold("Options:")}
|
|
|
5720
5813
|
}
|
|
5721
5814
|
if (stateExists) {
|
|
5722
5815
|
try {
|
|
5723
|
-
const stat =
|
|
5816
|
+
const stat = fs10.statSync(state.dir);
|
|
5724
5817
|
if (!stat.isDirectory()) {
|
|
5725
5818
|
add("fail", `State path exists but is not a directory: ${state.dir}`);
|
|
5726
5819
|
} else if (checkPathWritable(state.dir)) {
|
|
@@ -5740,7 +5833,7 @@ ${colors_default.bold("Options:")}
|
|
|
5740
5833
|
`State directory does not exist and no writable ancestor was found: ${state.dir}`
|
|
5741
5834
|
);
|
|
5742
5835
|
} else {
|
|
5743
|
-
const ancestorStat =
|
|
5836
|
+
const ancestorStat = fs10.statSync(ancestor);
|
|
5744
5837
|
if (!ancestorStat.isDirectory()) {
|
|
5745
5838
|
add("fail", `State directory does not exist and ancestor is not a directory: ${ancestor}`);
|
|
5746
5839
|
} else if (checkPathWritable(ancestor)) {
|
|
@@ -5766,9 +5859,9 @@ ${colors_default.bold("Options:")}
|
|
|
5766
5859
|
doctorProxyStartHint(proxyPort, proxyTls)
|
|
5767
5860
|
);
|
|
5768
5861
|
}
|
|
5769
|
-
if (
|
|
5862
|
+
if (fs10.existsSync(store.pidPath)) {
|
|
5770
5863
|
try {
|
|
5771
|
-
const rawPid =
|
|
5864
|
+
const rawPid = fs10.readFileSync(store.pidPath, "utf-8").trim();
|
|
5772
5865
|
const pid = parseInt(rawPid, 10);
|
|
5773
5866
|
if (isNaN(pid) || pid <= 0) {
|
|
5774
5867
|
add("fail", `Proxy PID file is invalid: ${store.pidPath}`);
|
|
@@ -5818,7 +5911,7 @@ ${colors_default.bold("Options:")}
|
|
|
5818
5911
|
add("info", "Generated local CA is not required for custom TLS certificates.");
|
|
5819
5912
|
} else if (proxyTls) {
|
|
5820
5913
|
const caPath = path9.join(state.dir, "ca.pem");
|
|
5821
|
-
if (
|
|
5914
|
+
if (fs10.existsSync(caPath)) {
|
|
5822
5915
|
if (isCATrusted(state.dir)) {
|
|
5823
5916
|
add("ok", "Local CA is trusted by the OS trust store.");
|
|
5824
5917
|
} else {
|
|
@@ -6243,7 +6336,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6243
6336
|
} else {
|
|
6244
6337
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
6245
6338
|
const logPath2 = path9.join(resolveStateDir(proxyPort), "proxy.log");
|
|
6246
|
-
if (
|
|
6339
|
+
if (fs10.existsSync(logPath2)) {
|
|
6247
6340
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
6248
6341
|
}
|
|
6249
6342
|
}
|
|
@@ -6282,8 +6375,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6282
6375
|
store.ensureDir();
|
|
6283
6376
|
if (customCertPath && customKeyPath) {
|
|
6284
6377
|
try {
|
|
6285
|
-
const cert =
|
|
6286
|
-
const key =
|
|
6378
|
+
const cert = fs10.readFileSync(customCertPath);
|
|
6379
|
+
const key = fs10.readFileSync(customKeyPath);
|
|
6287
6380
|
const certStr = cert.toString("utf-8");
|
|
6288
6381
|
const keyStr = key.toString("utf-8");
|
|
6289
6382
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -6328,9 +6421,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6328
6421
|
console.warn(colors_default.cyan(" portless trust"));
|
|
6329
6422
|
}
|
|
6330
6423
|
}
|
|
6331
|
-
const cert =
|
|
6332
|
-
const key =
|
|
6333
|
-
const ca =
|
|
6424
|
+
const cert = fs10.readFileSync(certs.certPath);
|
|
6425
|
+
const key = fs10.readFileSync(certs.keyPath);
|
|
6426
|
+
const ca = fs10.readFileSync(certs.caPath);
|
|
6334
6427
|
tlsOptions = {
|
|
6335
6428
|
cert,
|
|
6336
6429
|
key,
|
|
@@ -6355,10 +6448,10 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6355
6448
|
}
|
|
6356
6449
|
store.ensureDir();
|
|
6357
6450
|
const logPath = path9.join(stateDir, "proxy.log");
|
|
6358
|
-
const logFd =
|
|
6451
|
+
const logFd = fs10.openSync(logPath, "a");
|
|
6359
6452
|
try {
|
|
6360
6453
|
try {
|
|
6361
|
-
|
|
6454
|
+
fs10.chmodSync(logPath, FILE_MODE);
|
|
6362
6455
|
} catch {
|
|
6363
6456
|
}
|
|
6364
6457
|
fixOwnership(logPath);
|
|
@@ -6390,13 +6483,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6390
6483
|
});
|
|
6391
6484
|
child.unref();
|
|
6392
6485
|
} finally {
|
|
6393
|
-
|
|
6486
|
+
fs10.closeSync(logFd);
|
|
6394
6487
|
}
|
|
6395
6488
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
6396
6489
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
6397
6490
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
6398
6491
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
6399
|
-
if (
|
|
6492
|
+
if (fs10.existsSync(logPath)) {
|
|
6400
6493
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
6401
6494
|
}
|
|
6402
6495
|
process.exit(1);
|
|
@@ -6467,7 +6560,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
|
|
|
6467
6560
|
nameSource = inferred.source;
|
|
6468
6561
|
}
|
|
6469
6562
|
const worktree = detectWorktreePrefix(cwd);
|
|
6470
|
-
const effectiveName =
|
|
6563
|
+
const effectiveName = applyWorktreePrefix(baseName, worktree);
|
|
6471
6564
|
const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
|
|
6472
6565
|
const store = new RouteStore(dir, {
|
|
6473
6566
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
@@ -6549,7 +6642,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes)
|
|
|
6549
6642
|
};
|
|
6550
6643
|
if (tls2) {
|
|
6551
6644
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
6552
|
-
if (
|
|
6645
|
+
if (fs10.existsSync(caPath)) {
|
|
6553
6646
|
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
6554
6647
|
}
|
|
6555
6648
|
}
|
|
@@ -6627,6 +6720,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
|
|
|
6627
6720
|
}
|
|
6628
6721
|
}
|
|
6629
6722
|
const apps = [];
|
|
6723
|
+
const worktree = detectWorktreePrefix(wsRoot);
|
|
6630
6724
|
for (const pkg of packages) {
|
|
6631
6725
|
const rel = path9.relative(wsRoot, pkg.dir).replace(/\\/g, "/");
|
|
6632
6726
|
const rootOverride = loaded ? resolveAppConfig(loaded.config, loaded.configDir, pkg.dir) : null;
|
|
@@ -6668,6 +6762,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
|
|
|
6668
6762
|
name = pkgLabel === projectName ? projectName : `${pkgLabel}.${projectName}`;
|
|
6669
6763
|
label = pkg.scope ? `@${pkg.scope}/${pkg.name}` : pkg.name ?? rel;
|
|
6670
6764
|
}
|
|
6765
|
+
name = applyWorktreePrefix(name, worktree);
|
|
6671
6766
|
apps.push({ pkg, name, label, commandArgs, appPort: appOverride.appPort, proxied });
|
|
6672
6767
|
}
|
|
6673
6768
|
if (apps.length === 0) {
|
|
@@ -6737,7 +6832,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName,
|
|
|
6737
6832
|
};
|
|
6738
6833
|
if (tls2) {
|
|
6739
6834
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
6740
|
-
if (
|
|
6835
|
+
if (fs10.existsSync(caPath)) {
|
|
6741
6836
|
entry.NODE_EXTRA_CA_CERTS = caPath;
|
|
6742
6837
|
}
|
|
6743
6838
|
}
|