portless 0.15.2 → 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-T7CGCIRO.js → chunk-JSJUKQRJ.js} +34 -7
- package/dist/cli.js +372 -283
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
isProcessAlive,
|
|
17
17
|
parseHostname,
|
|
18
18
|
parseHostnames,
|
|
19
|
+
resolveUserHome,
|
|
19
20
|
shouldAutoSyncHosts,
|
|
20
21
|
syncHostsFile
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-JSJUKQRJ.js";
|
|
22
23
|
|
|
23
24
|
// src/colors.ts
|
|
24
25
|
function supportsColor() {
|
|
@@ -44,18 +45,96 @@ var gray = dim;
|
|
|
44
45
|
var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
|
|
45
46
|
|
|
46
47
|
// src/cli.ts
|
|
47
|
-
import * as
|
|
48
|
+
import * as fs10 from "fs";
|
|
48
49
|
import * as path9 from "path";
|
|
49
50
|
import { spawn as spawn4, spawnSync as spawnSync5 } from "child_process";
|
|
50
51
|
import { StringDecoder } from "string_decoder";
|
|
51
52
|
|
|
52
53
|
// src/certs.ts
|
|
53
|
-
import * as
|
|
54
|
+
import * as fs2 from "fs";
|
|
54
55
|
import * as path from "path";
|
|
55
|
-
import * as
|
|
56
|
+
import * as crypto2 from "crypto";
|
|
56
57
|
import * as tls from "tls";
|
|
57
|
-
import { execFile as execFileCb, execFileSync } from "child_process";
|
|
58
|
+
import { execFile as execFileCb, execFileSync as execFileSync2 } from "child_process";
|
|
58
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
|
|
59
138
|
var CA_VALIDITY_DAYS = 3650;
|
|
60
139
|
var SERVER_VALIDITY_DAYS = 365;
|
|
61
140
|
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -69,9 +148,10 @@ var CA_CERT_FILE = "ca.pem";
|
|
|
69
148
|
var SERVER_KEY_FILE = "server-key.pem";
|
|
70
149
|
var SERVER_CERT_FILE = "server.pem";
|
|
71
150
|
var CA_TRUST_MARKER = "ca.trusted";
|
|
151
|
+
var CA_TRUST_REFRESH_PENDING = "ca.trust-refresh-pending";
|
|
72
152
|
function fileExists(filePath) {
|
|
73
153
|
try {
|
|
74
|
-
|
|
154
|
+
fs2.accessSync(filePath, fs2.constants.R_OK);
|
|
75
155
|
return true;
|
|
76
156
|
} catch {
|
|
77
157
|
return false;
|
|
@@ -80,30 +160,38 @@ function fileExists(filePath) {
|
|
|
80
160
|
function caFingerprint(stateDir) {
|
|
81
161
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
82
162
|
try {
|
|
83
|
-
const pem =
|
|
84
|
-
return
|
|
163
|
+
const pem = fs2.readFileSync(caCertPath);
|
|
164
|
+
return crypto2.createHash("sha256").update(pem).digest("hex");
|
|
85
165
|
} catch {
|
|
86
166
|
return null;
|
|
87
167
|
}
|
|
88
168
|
}
|
|
89
169
|
function readTrustMarker(stateDir) {
|
|
90
170
|
try {
|
|
91
|
-
const value =
|
|
171
|
+
const value = fs2.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
|
|
92
172
|
return value || null;
|
|
93
173
|
} catch {
|
|
94
174
|
return null;
|
|
95
175
|
}
|
|
96
176
|
}
|
|
177
|
+
function clearTrustRefreshPending(stateDir) {
|
|
178
|
+
try {
|
|
179
|
+
fs2.unlinkSync(path.join(stateDir, CA_TRUST_REFRESH_PENDING));
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
97
183
|
function writeTrustMarker(stateDir) {
|
|
98
184
|
const fp = caFingerprint(stateDir);
|
|
99
185
|
if (fp) {
|
|
100
|
-
|
|
186
|
+
clearTrustRefreshPending(stateDir);
|
|
187
|
+
const marker = isWSL() ? `wsl:${fp}` : fp;
|
|
188
|
+
fs2.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), marker + "\n");
|
|
101
189
|
fixOwnership(path.join(stateDir, CA_TRUST_MARKER));
|
|
102
190
|
}
|
|
103
191
|
}
|
|
104
192
|
function clearTrustMarker(stateDir) {
|
|
105
193
|
try {
|
|
106
|
-
|
|
194
|
+
fs2.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
|
|
107
195
|
} catch {
|
|
108
196
|
}
|
|
109
197
|
}
|
|
@@ -143,8 +231,8 @@ function opensslErrorMessage() {
|
|
|
143
231
|
}
|
|
144
232
|
function isCertValid(certPath) {
|
|
145
233
|
try {
|
|
146
|
-
const pem =
|
|
147
|
-
const cert = new
|
|
234
|
+
const pem = fs2.readFileSync(certPath, "utf-8");
|
|
235
|
+
const cert = new crypto2.X509Certificate(pem);
|
|
148
236
|
const expiry = new Date(cert.validTo).getTime();
|
|
149
237
|
return Date.now() + EXPIRY_BUFFER_MS < expiry;
|
|
150
238
|
} catch {
|
|
@@ -173,7 +261,7 @@ function isCertSignatureStrong(certPath) {
|
|
|
173
261
|
function openssl(args, options) {
|
|
174
262
|
try {
|
|
175
263
|
const extraEnv = getOpensslEnv();
|
|
176
|
-
return
|
|
264
|
+
return execFileSync2("openssl", args, {
|
|
177
265
|
encoding: "utf-8",
|
|
178
266
|
timeout: OPENSSL_TIMEOUT_MS,
|
|
179
267
|
input: options?.input,
|
|
@@ -226,8 +314,8 @@ function generateCA(stateDir) {
|
|
|
226
314
|
"-addext",
|
|
227
315
|
"keyUsage=critical,keyCertSign,cRLSign"
|
|
228
316
|
]);
|
|
229
|
-
|
|
230
|
-
|
|
317
|
+
fs2.chmodSync(keyPath, 384);
|
|
318
|
+
fs2.chmodSync(certPath, 420);
|
|
231
319
|
fixOwnership(keyPath, certPath);
|
|
232
320
|
clearTrustMarker(stateDir);
|
|
233
321
|
return { certPath, keyPath };
|
|
@@ -242,7 +330,7 @@ function generateServerCert(stateDir) {
|
|
|
242
330
|
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
243
331
|
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
244
332
|
const sans = ["DNS:localhost", "DNS:*.localhost", "DNS:*.local"];
|
|
245
|
-
|
|
333
|
+
fs2.writeFileSync(
|
|
246
334
|
extPath,
|
|
247
335
|
[
|
|
248
336
|
"authorityKeyIdentifier=keyid,issuer",
|
|
@@ -254,9 +342,9 @@ function generateServerCert(stateDir) {
|
|
|
254
342
|
);
|
|
255
343
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
256
344
|
if (!fileExists(srlPath)) {
|
|
257
|
-
|
|
345
|
+
fs2.writeFileSync(
|
|
258
346
|
srlPath,
|
|
259
|
-
|
|
347
|
+
crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
260
348
|
);
|
|
261
349
|
}
|
|
262
350
|
openssl([
|
|
@@ -280,12 +368,12 @@ function generateServerCert(stateDir) {
|
|
|
280
368
|
]);
|
|
281
369
|
for (const tmp of [csrPath, extPath]) {
|
|
282
370
|
try {
|
|
283
|
-
|
|
371
|
+
fs2.unlinkSync(tmp);
|
|
284
372
|
} catch {
|
|
285
373
|
}
|
|
286
374
|
}
|
|
287
|
-
|
|
288
|
-
|
|
375
|
+
fs2.chmodSync(serverKeyPath, 384);
|
|
376
|
+
fs2.chmodSync(serverCertPath, 420);
|
|
289
377
|
fixOwnership(serverKeyPath, serverCertPath);
|
|
290
378
|
return { certPath: serverCertPath, keyPath: serverKeyPath };
|
|
291
379
|
}
|
|
@@ -316,36 +404,30 @@ function isCATrusted(stateDir) {
|
|
|
316
404
|
const marker = readTrustMarker(stateDir);
|
|
317
405
|
if (marker) {
|
|
318
406
|
const fp = caFingerprint(stateDir);
|
|
319
|
-
|
|
407
|
+
const expected = fp && isWSL() ? `wsl:${fp}` : fp;
|
|
408
|
+
if (expected && marker === expected) return true;
|
|
320
409
|
}
|
|
321
410
|
if (process.platform === "darwin") {
|
|
322
411
|
return isCATrustedMacOS(caCertPath);
|
|
323
412
|
} else if (process.platform === "linux") {
|
|
324
|
-
|
|
413
|
+
if (!isCATrustedLinux(stateDir)) return false;
|
|
414
|
+
if (!isWSL()) return true;
|
|
415
|
+
try {
|
|
416
|
+
return isWindowsCATrusted(caCertPath, wslWindowsCAStoreOptions());
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
325
420
|
} else if (process.platform === "win32") {
|
|
326
|
-
return
|
|
421
|
+
return isWindowsCATrusted(caCertPath);
|
|
327
422
|
}
|
|
328
423
|
return false;
|
|
329
424
|
}
|
|
330
|
-
function isCATrustedWindows(caCertPath) {
|
|
331
|
-
try {
|
|
332
|
-
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
333
|
-
const result = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
334
|
-
encoding: "utf-8",
|
|
335
|
-
timeout: 1e4,
|
|
336
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
337
|
-
});
|
|
338
|
-
return result.replace(/\s/g, "").toLowerCase().includes(fingerprint);
|
|
339
|
-
} catch {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
425
|
function isCATrustedMacOS(caCertPath) {
|
|
344
426
|
try {
|
|
345
427
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
346
428
|
const sudoUser = process.env.SUDO_USER;
|
|
347
429
|
if (isRoot && sudoUser) {
|
|
348
|
-
|
|
430
|
+
execFileSync2(
|
|
349
431
|
"sudo",
|
|
350
432
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
351
433
|
{
|
|
@@ -354,7 +436,7 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
354
436
|
}
|
|
355
437
|
);
|
|
356
438
|
} else {
|
|
357
|
-
|
|
439
|
+
execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
358
440
|
stdio: "pipe",
|
|
359
441
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
360
442
|
});
|
|
@@ -366,7 +448,7 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
366
448
|
}
|
|
367
449
|
function loginKeychainPath() {
|
|
368
450
|
try {
|
|
369
|
-
const result =
|
|
451
|
+
const result = execFileSync2("security", ["default-keychain"], {
|
|
370
452
|
encoding: "utf-8",
|
|
371
453
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
372
454
|
}).trim();
|
|
@@ -397,7 +479,7 @@ var LINUX_CA_TRUST_CONFIGS = {
|
|
|
397
479
|
};
|
|
398
480
|
function detectLinuxDistro() {
|
|
399
481
|
try {
|
|
400
|
-
const osRelease =
|
|
482
|
+
const osRelease = fs2.readFileSync("/etc/os-release", "utf-8").toLowerCase();
|
|
401
483
|
if (osRelease.includes("arch")) return "arch";
|
|
402
484
|
if (osRelease.includes("fedora") || osRelease.includes("rhel") || osRelease.includes("centos"))
|
|
403
485
|
return "fedora";
|
|
@@ -407,8 +489,8 @@ function detectLinuxDistro() {
|
|
|
407
489
|
}
|
|
408
490
|
for (const [distro, config] of Object.entries(LINUX_CA_TRUST_CONFIGS)) {
|
|
409
491
|
try {
|
|
410
|
-
|
|
411
|
-
if (
|
|
492
|
+
execFileSync2("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
|
|
493
|
+
if (fs2.existsSync(path.dirname(config.certDir))) return distro;
|
|
412
494
|
} catch {
|
|
413
495
|
}
|
|
414
496
|
}
|
|
@@ -418,13 +500,12 @@ function getLinuxCATrustConfig() {
|
|
|
418
500
|
const distro = detectLinuxDistro();
|
|
419
501
|
return LINUX_CA_TRUST_CONFIGS[distro ?? "debian"];
|
|
420
502
|
}
|
|
421
|
-
function isCATrustedLinux(stateDir) {
|
|
422
|
-
const config = getLinuxCATrustConfig();
|
|
503
|
+
function isCATrustedLinux(stateDir, config = getLinuxCATrustConfig()) {
|
|
423
504
|
const systemCertPath = path.join(config.certDir, "portless-ca.crt");
|
|
424
505
|
if (!fileExists(systemCertPath)) return false;
|
|
425
506
|
try {
|
|
426
|
-
const ours =
|
|
427
|
-
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();
|
|
428
509
|
return ours === installed;
|
|
429
510
|
} catch {
|
|
430
511
|
return false;
|
|
@@ -439,8 +520,8 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
439
520
|
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
440
521
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
441
522
|
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
442
|
-
if (!
|
|
443
|
-
await
|
|
523
|
+
if (!fs2.existsSync(hostDir)) {
|
|
524
|
+
await fs2.promises.mkdir(hostDir, { recursive: true, mode: 493 });
|
|
444
525
|
fixOwnership(hostDir);
|
|
445
526
|
}
|
|
446
527
|
const safeName = sanitizeHostForFilename(hostname);
|
|
@@ -456,7 +537,7 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
456
537
|
if (parts.length >= 2) {
|
|
457
538
|
sans.push(`DNS:*.${parts.slice(1).join(".")}`);
|
|
458
539
|
}
|
|
459
|
-
await
|
|
540
|
+
await fs2.promises.writeFile(
|
|
460
541
|
extPath,
|
|
461
542
|
[
|
|
462
543
|
"authorityKeyIdentifier=keyid,issuer",
|
|
@@ -467,10 +548,10 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
467
548
|
].join("\n") + "\n"
|
|
468
549
|
);
|
|
469
550
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
470
|
-
if (!
|
|
471
|
-
await
|
|
551
|
+
if (!fs2.existsSync(srlPath)) {
|
|
552
|
+
await fs2.promises.writeFile(
|
|
472
553
|
srlPath,
|
|
473
|
-
|
|
554
|
+
crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
474
555
|
);
|
|
475
556
|
}
|
|
476
557
|
await opensslAsync([
|
|
@@ -494,12 +575,12 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
494
575
|
]);
|
|
495
576
|
for (const tmp of [csrPath, extPath]) {
|
|
496
577
|
try {
|
|
497
|
-
await
|
|
578
|
+
await fs2.promises.unlink(tmp);
|
|
498
579
|
} catch {
|
|
499
580
|
}
|
|
500
581
|
}
|
|
501
|
-
await
|
|
502
|
-
await
|
|
582
|
+
await fs2.promises.chmod(keyPath, 384);
|
|
583
|
+
await fs2.promises.chmod(certPath, 420);
|
|
503
584
|
fixOwnership(keyPath, certPath);
|
|
504
585
|
return { certPath, keyPath };
|
|
505
586
|
}
|
|
@@ -526,10 +607,10 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
|
|
|
526
607
|
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
527
608
|
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
|
|
528
609
|
try {
|
|
529
|
-
const hostCert =
|
|
610
|
+
const hostCert = fs2.readFileSync(certPath);
|
|
530
611
|
const ctx = tls.createSecureContext({
|
|
531
612
|
cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
|
|
532
|
-
key:
|
|
613
|
+
key: fs2.readFileSync(keyPath)
|
|
533
614
|
});
|
|
534
615
|
cache.set(servername, ctx);
|
|
535
616
|
cb(null, ctx);
|
|
@@ -543,8 +624,8 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
|
|
|
543
624
|
}
|
|
544
625
|
const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
|
|
545
626
|
const [hostCert, key] = await Promise.all([
|
|
546
|
-
|
|
547
|
-
|
|
627
|
+
fs2.promises.readFile(generated.certPath),
|
|
628
|
+
fs2.promises.readFile(generated.keyPath)
|
|
548
629
|
]);
|
|
549
630
|
return tls.createSecureContext({
|
|
550
631
|
cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
|
|
@@ -574,7 +655,7 @@ function trustCA(stateDir) {
|
|
|
574
655
|
if (process.platform === "darwin") {
|
|
575
656
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
576
657
|
if (isRoot) {
|
|
577
|
-
|
|
658
|
+
execFileSync2(
|
|
578
659
|
"security",
|
|
579
660
|
[
|
|
580
661
|
"add-trusted-cert",
|
|
@@ -589,7 +670,7 @@ function trustCA(stateDir) {
|
|
|
589
670
|
);
|
|
590
671
|
} else {
|
|
591
672
|
const keychain = loginKeychainPath();
|
|
592
|
-
|
|
673
|
+
execFileSync2(
|
|
593
674
|
"security",
|
|
594
675
|
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
595
676
|
{ stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
|
|
@@ -599,19 +680,19 @@ function trustCA(stateDir) {
|
|
|
599
680
|
return { trusted: true };
|
|
600
681
|
} else if (process.platform === "linux") {
|
|
601
682
|
const config = getLinuxCATrustConfig();
|
|
602
|
-
if (!
|
|
603
|
-
|
|
683
|
+
if (!fs2.existsSync(config.certDir)) {
|
|
684
|
+
fs2.mkdirSync(config.certDir, { recursive: true });
|
|
604
685
|
}
|
|
605
686
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
606
|
-
|
|
607
|
-
|
|
687
|
+
fs2.copyFileSync(caCertPath, dest);
|
|
688
|
+
execFileSync2(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
689
|
+
if (isWSL()) {
|
|
690
|
+
trustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
|
|
691
|
+
}
|
|
608
692
|
writeTrustMarker(stateDir);
|
|
609
693
|
return { trusted: true };
|
|
610
694
|
} else if (process.platform === "win32") {
|
|
611
|
-
|
|
612
|
-
stdio: "pipe",
|
|
613
|
-
timeout: 3e4
|
|
614
|
-
});
|
|
695
|
+
trustWindowsCA(caCertPath);
|
|
615
696
|
writeTrustMarker(stateDir);
|
|
616
697
|
return { trusted: true };
|
|
617
698
|
}
|
|
@@ -637,18 +718,15 @@ function untrustCA(stateDir) {
|
|
|
637
718
|
clearTrustMarker(stateDir);
|
|
638
719
|
return { removed: true };
|
|
639
720
|
}
|
|
640
|
-
|
|
641
|
-
clearTrustMarker(stateDir);
|
|
642
|
-
return { removed: true };
|
|
643
|
-
}
|
|
721
|
+
const runningInWSL = isWSL();
|
|
644
722
|
try {
|
|
645
723
|
let result;
|
|
646
724
|
if (process.platform === "darwin") {
|
|
647
725
|
result = untrustCAMacOS(caCertPath);
|
|
648
726
|
} else if (process.platform === "linux") {
|
|
649
|
-
result = untrustCALinux(stateDir);
|
|
727
|
+
result = runningInWSL ? untrustCAWSL(stateDir, caCertPath) : untrustCALinux(stateDir);
|
|
650
728
|
} else if (process.platform === "win32") {
|
|
651
|
-
result =
|
|
729
|
+
result = untrustWindowsCA(caCertPath);
|
|
652
730
|
} else {
|
|
653
731
|
result = { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
654
732
|
}
|
|
@@ -663,7 +741,7 @@ function untrustCAMacOS(caCertPath) {
|
|
|
663
741
|
const errors = [];
|
|
664
742
|
const tryExec = (args) => {
|
|
665
743
|
try {
|
|
666
|
-
|
|
744
|
+
execFileSync2("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
|
|
667
745
|
return true;
|
|
668
746
|
} catch (err) {
|
|
669
747
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -685,13 +763,13 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
|
685
763
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
686
764
|
const sudoUser = process.env.SUDO_USER;
|
|
687
765
|
if (isRoot && sudoUser) {
|
|
688
|
-
|
|
766
|
+
execFileSync2(
|
|
689
767
|
"sudo",
|
|
690
768
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
691
769
|
{ stdio: "pipe", timeout: MACOS_SECURITY_TIMEOUT_MS }
|
|
692
770
|
);
|
|
693
771
|
} else {
|
|
694
|
-
|
|
772
|
+
execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
695
773
|
stdio: "pipe",
|
|
696
774
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
697
775
|
});
|
|
@@ -701,63 +779,67 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
|
701
779
|
return false;
|
|
702
780
|
}
|
|
703
781
|
}
|
|
704
|
-
function untrustCALinux(stateDir) {
|
|
782
|
+
function untrustCALinux(stateDir, options = {}) {
|
|
705
783
|
const errors = [];
|
|
706
|
-
|
|
707
|
-
|
|
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) {
|
|
708
792
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
709
793
|
try {
|
|
710
794
|
if (fileExists(dest)) {
|
|
711
|
-
const ours =
|
|
712
|
-
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();
|
|
713
797
|
if (ours === installed) {
|
|
714
|
-
|
|
715
|
-
|
|
798
|
+
if (!refreshNeeded) {
|
|
799
|
+
fs2.writeFileSync(pendingRefreshPath, "1\n");
|
|
800
|
+
fixOwnership(pendingRefreshPath);
|
|
801
|
+
refreshNeeded = true;
|
|
802
|
+
}
|
|
803
|
+
fs2.unlinkSync(dest);
|
|
716
804
|
}
|
|
717
805
|
}
|
|
718
806
|
} catch (err) {
|
|
719
807
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
720
808
|
}
|
|
721
809
|
}
|
|
722
|
-
if (
|
|
810
|
+
if (refreshNeeded) {
|
|
723
811
|
try {
|
|
724
|
-
|
|
725
|
-
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
812
|
+
runUpdate(activeConfig.updateCommand);
|
|
726
813
|
} catch (err) {
|
|
727
814
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
728
815
|
}
|
|
729
816
|
}
|
|
730
|
-
if (
|
|
817
|
+
if (errors.length > 0 || isCATrustedLinux(stateDir, activeConfig)) {
|
|
731
818
|
return {
|
|
732
819
|
removed: false,
|
|
733
820
|
error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
|
|
734
821
|
};
|
|
735
822
|
}
|
|
823
|
+
clearTrustRefreshPending(stateDir);
|
|
736
824
|
return { removed: true };
|
|
737
825
|
}
|
|
738
|
-
function
|
|
826
|
+
function untrustCAWSL(stateDir, caCertPath) {
|
|
827
|
+
const linuxResult = untrustCALinux(stateDir);
|
|
828
|
+
let windowsResult;
|
|
739
829
|
try {
|
|
740
|
-
|
|
741
|
-
const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
742
|
-
encoding: "utf-8",
|
|
743
|
-
timeout: 1e4,
|
|
744
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
745
|
-
});
|
|
746
|
-
const normalized = storeListing.replace(/\s/g, "").toLowerCase();
|
|
747
|
-
if (!normalized.includes(fingerprint)) {
|
|
748
|
-
return { removed: true };
|
|
749
|
-
}
|
|
750
|
-
execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
|
|
751
|
-
stdio: "pipe",
|
|
752
|
-
timeout: 3e4
|
|
753
|
-
});
|
|
754
|
-
if (isCATrustedWindows(caCertPath)) {
|
|
755
|
-
return { removed: false, error: "certutil could not remove the portless CA from Root" };
|
|
756
|
-
}
|
|
757
|
-
return { removed: true };
|
|
830
|
+
windowsResult = untrustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
|
|
758
831
|
} catch (err) {
|
|
759
|
-
|
|
832
|
+
windowsResult = {
|
|
833
|
+
removed: false,
|
|
834
|
+
error: err instanceof Error ? err.message : String(err)
|
|
835
|
+
};
|
|
760
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
|
+
};
|
|
761
843
|
}
|
|
762
844
|
|
|
763
845
|
// src/tailscale.ts
|
|
@@ -766,7 +848,7 @@ var TAILSCALE_BINARY = "tailscale";
|
|
|
766
848
|
var TAILSCALE_COMMAND_TIMEOUT_MS = 3e4;
|
|
767
849
|
var PREFERRED_SERVE_PORTS = [443, 8443, 8444, 8445, 8446, 8447, 8448, 8449, 8450];
|
|
768
850
|
var FUNNEL_PORTS = [443, 8443, 1e4];
|
|
769
|
-
function
|
|
851
|
+
function defaultRunner2(args) {
|
|
770
852
|
const result = spawnSync(TAILSCALE_BINARY, args, {
|
|
771
853
|
encoding: "utf-8",
|
|
772
854
|
killSignal: "SIGKILL",
|
|
@@ -858,7 +940,7 @@ function throwFunnelNotEnabled(status) {
|
|
|
858
940
|
);
|
|
859
941
|
}
|
|
860
942
|
function ensureTailscaleReady(options = {}) {
|
|
861
|
-
const runner = options.runner ??
|
|
943
|
+
const runner = options.runner ?? defaultRunner2;
|
|
862
944
|
runOrThrow(["version"], "check tailscale version", runner);
|
|
863
945
|
const statusResult = runOrThrow(["status", "--json"], "read tailscale status", runner);
|
|
864
946
|
const status = parseStatusJson(statusResult.stdout);
|
|
@@ -874,7 +956,7 @@ function ensureTailscaleReady(options = {}) {
|
|
|
874
956
|
baseUrl: `https://${dnsName}`
|
|
875
957
|
};
|
|
876
958
|
}
|
|
877
|
-
function getUsedServePorts(runner =
|
|
959
|
+
function getUsedServePorts(runner = defaultRunner2) {
|
|
878
960
|
const result = runner(["serve", "status", "--json"]);
|
|
879
961
|
if (result.error || result.status !== 0) {
|
|
880
962
|
return /* @__PURE__ */ new Set();
|
|
@@ -970,7 +1052,7 @@ function register(mode, localPort, httpsPort, runner) {
|
|
|
970
1052
|
}
|
|
971
1053
|
}
|
|
972
1054
|
function unregister(mode, httpsPort, options) {
|
|
973
|
-
const runner = options?.runner ??
|
|
1055
|
+
const runner = options?.runner ?? defaultRunner2;
|
|
974
1056
|
const result = runner([mode, "--yes", `--https=${httpsPort}`, "off"]);
|
|
975
1057
|
if (result.error) {
|
|
976
1058
|
const errno = result.error;
|
|
@@ -989,10 +1071,10 @@ ${result.stdout}`.toLowerCase();
|
|
|
989
1071
|
}
|
|
990
1072
|
}
|
|
991
1073
|
function registerServe(localPort, httpsPort, options) {
|
|
992
|
-
register("serve", localPort, httpsPort, options?.runner ??
|
|
1074
|
+
register("serve", localPort, httpsPort, options?.runner ?? defaultRunner2);
|
|
993
1075
|
}
|
|
994
1076
|
function registerFunnel(localPort, httpsPort, options) {
|
|
995
|
-
register("funnel", localPort, httpsPort, options?.runner ??
|
|
1077
|
+
register("funnel", localPort, httpsPort, options?.runner ?? defaultRunner2);
|
|
996
1078
|
}
|
|
997
1079
|
function unregisterTailscale(route) {
|
|
998
1080
|
if (!route.tailscaleHttpsPort) return;
|
|
@@ -1017,7 +1099,7 @@ function defaultSpawner(args) {
|
|
|
1017
1099
|
windowsHide: true
|
|
1018
1100
|
});
|
|
1019
1101
|
}
|
|
1020
|
-
function
|
|
1102
|
+
function defaultRunner3(args) {
|
|
1021
1103
|
const result = spawnSync2(NGROK_BINARY, args, {
|
|
1022
1104
|
encoding: "utf-8",
|
|
1023
1105
|
killSignal: "SIGKILL",
|
|
@@ -1054,7 +1136,7 @@ function formatOutputError(output) {
|
|
|
1054
1136
|
`Failed to start ngrok tunnel: ${details || "ngrok exited before printing a public URL"}`
|
|
1055
1137
|
);
|
|
1056
1138
|
}
|
|
1057
|
-
function ensureNgrokAvailable(runner =
|
|
1139
|
+
function ensureNgrokAvailable(runner = defaultRunner3) {
|
|
1058
1140
|
const result = runner(["version"]);
|
|
1059
1141
|
if (result.error) {
|
|
1060
1142
|
throw formatSpawnError(result.error);
|
|
@@ -1179,8 +1261,8 @@ function stopNgrok(route) {
|
|
|
1179
1261
|
|
|
1180
1262
|
// src/auto.ts
|
|
1181
1263
|
import { createHash as createHash2 } from "crypto";
|
|
1182
|
-
import { execFileSync as
|
|
1183
|
-
import * as
|
|
1264
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1265
|
+
import * as fs3 from "fs";
|
|
1184
1266
|
import * as path2 from "path";
|
|
1185
1267
|
var MAX_DNS_LABEL_LENGTH = 63;
|
|
1186
1268
|
function truncateLabel(label) {
|
|
@@ -1220,7 +1302,7 @@ function findPackageJsonName(startDir) {
|
|
|
1220
1302
|
for (; ; ) {
|
|
1221
1303
|
const pkgPath = path2.join(dir, "package.json");
|
|
1222
1304
|
try {
|
|
1223
|
-
const raw =
|
|
1305
|
+
const raw = fs3.readFileSync(pkgPath, "utf-8");
|
|
1224
1306
|
const pkg = JSON.parse(raw);
|
|
1225
1307
|
if (typeof pkg.name === "string" && pkg.name) {
|
|
1226
1308
|
return pkg.name.replace(/^@[^/]+\//, "");
|
|
@@ -1235,7 +1317,7 @@ function findPackageJsonName(startDir) {
|
|
|
1235
1317
|
}
|
|
1236
1318
|
function findGitRoot(startDir) {
|
|
1237
1319
|
try {
|
|
1238
|
-
const toplevel =
|
|
1320
|
+
const toplevel = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
1239
1321
|
cwd: startDir,
|
|
1240
1322
|
encoding: "utf-8",
|
|
1241
1323
|
timeout: 5e3,
|
|
@@ -1248,7 +1330,7 @@ function findGitRoot(startDir) {
|
|
|
1248
1330
|
for (; ; ) {
|
|
1249
1331
|
const gitPath = path2.join(dir, ".git");
|
|
1250
1332
|
try {
|
|
1251
|
-
const stat =
|
|
1333
|
+
const stat = fs3.statSync(gitPath);
|
|
1252
1334
|
if (stat.isDirectory()) return dir;
|
|
1253
1335
|
if (stat.isFile()) return dir;
|
|
1254
1336
|
} catch {
|
|
@@ -1276,7 +1358,7 @@ function detectWorktreePrefix(cwd = process.cwd()) {
|
|
|
1276
1358
|
}
|
|
1277
1359
|
function detectWorktreeViaCli(cwd) {
|
|
1278
1360
|
try {
|
|
1279
|
-
const listOutput =
|
|
1361
|
+
const listOutput = execFileSync3("git", ["worktree", "list", "--porcelain"], {
|
|
1280
1362
|
cwd,
|
|
1281
1363
|
encoding: "utf-8",
|
|
1282
1364
|
timeout: 5e3,
|
|
@@ -1286,7 +1368,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1286
1368
|
if (worktreeCount <= 1) return null;
|
|
1287
1369
|
const gitDir = path2.resolve(
|
|
1288
1370
|
cwd,
|
|
1289
|
-
|
|
1371
|
+
execFileSync3("git", ["rev-parse", "--git-dir"], {
|
|
1290
1372
|
cwd,
|
|
1291
1373
|
encoding: "utf-8",
|
|
1292
1374
|
timeout: 5e3,
|
|
@@ -1295,7 +1377,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1295
1377
|
);
|
|
1296
1378
|
const gitCommonDir = path2.resolve(
|
|
1297
1379
|
cwd,
|
|
1298
|
-
|
|
1380
|
+
execFileSync3("git", ["rev-parse", "--git-common-dir"], {
|
|
1299
1381
|
cwd,
|
|
1300
1382
|
encoding: "utf-8",
|
|
1301
1383
|
timeout: 5e3,
|
|
@@ -1303,7 +1385,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1303
1385
|
}).trim()
|
|
1304
1386
|
);
|
|
1305
1387
|
if (gitDir === gitCommonDir) return null;
|
|
1306
|
-
const branch =
|
|
1388
|
+
const branch = execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1307
1389
|
cwd,
|
|
1308
1390
|
encoding: "utf-8",
|
|
1309
1391
|
timeout: 5e3,
|
|
@@ -1321,12 +1403,12 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
1321
1403
|
for (; ; ) {
|
|
1322
1404
|
const gitPath = path2.join(dir, ".git");
|
|
1323
1405
|
try {
|
|
1324
|
-
const stat =
|
|
1406
|
+
const stat = fs3.statSync(gitPath);
|
|
1325
1407
|
if (stat.isDirectory()) {
|
|
1326
1408
|
return null;
|
|
1327
1409
|
}
|
|
1328
1410
|
if (stat.isFile()) {
|
|
1329
|
-
const content =
|
|
1411
|
+
const content = fs3.readFileSync(gitPath, "utf-8").trim();
|
|
1330
1412
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
1331
1413
|
if (!match) return null;
|
|
1332
1414
|
const gitdir = match[1];
|
|
@@ -1346,7 +1428,7 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
1346
1428
|
}
|
|
1347
1429
|
function readBranchFromHead(gitdir) {
|
|
1348
1430
|
try {
|
|
1349
|
-
const head =
|
|
1431
|
+
const head = fs3.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
|
|
1350
1432
|
const refMatch = head.match(/^ref: refs\/heads\/(.+)$/);
|
|
1351
1433
|
return refMatch ? refMatch[1] : null;
|
|
1352
1434
|
} catch {
|
|
@@ -1355,11 +1437,11 @@ function readBranchFromHead(gitdir) {
|
|
|
1355
1437
|
}
|
|
1356
1438
|
|
|
1357
1439
|
// src/cli-utils.ts
|
|
1358
|
-
import * as
|
|
1440
|
+
import * as fs4 from "fs";
|
|
1359
1441
|
import * as http from "http";
|
|
1360
1442
|
import * as https from "https";
|
|
1361
1443
|
import * as net from "net";
|
|
1362
|
-
import * as
|
|
1444
|
+
import * as os2 from "os";
|
|
1363
1445
|
import * as path3 from "path";
|
|
1364
1446
|
import * as readline from "readline";
|
|
1365
1447
|
import { execSync, spawn as spawn2 } from "child_process";
|
|
@@ -1368,8 +1450,8 @@ var FALLBACK_PROXY_PORT = 1355;
|
|
|
1368
1450
|
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
1369
1451
|
var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
|
|
1370
1452
|
var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
|
|
1371
|
-
var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(
|
|
1372
|
-
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");
|
|
1373
1455
|
var MIN_APP_PORT = 4e3;
|
|
1374
1456
|
var MAX_APP_PORT = 4999;
|
|
1375
1457
|
var RANDOM_PORT_ATTEMPTS = 50;
|
|
@@ -1504,7 +1586,7 @@ function resolveStateDir(_port) {
|
|
|
1504
1586
|
}
|
|
1505
1587
|
function readPortFromDir(dir) {
|
|
1506
1588
|
try {
|
|
1507
|
-
const raw =
|
|
1589
|
+
const raw = fs4.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
|
|
1508
1590
|
const port = parseInt(raw, 10);
|
|
1509
1591
|
return isNaN(port) ? null : port;
|
|
1510
1592
|
} catch {
|
|
@@ -1515,7 +1597,7 @@ var TLS_MARKER_FILE = "proxy.tls";
|
|
|
1515
1597
|
var CUSTOM_CERT_MARKER_FILE = "proxy.custom-cert";
|
|
1516
1598
|
function readTlsMarker(dir) {
|
|
1517
1599
|
try {
|
|
1518
|
-
return
|
|
1600
|
+
return fs4.existsSync(path3.join(dir, TLS_MARKER_FILE));
|
|
1519
1601
|
} catch {
|
|
1520
1602
|
return false;
|
|
1521
1603
|
}
|
|
@@ -1523,17 +1605,17 @@ function readTlsMarker(dir) {
|
|
|
1523
1605
|
function writeTlsMarker(dir, enabled2) {
|
|
1524
1606
|
const markerPath = path3.join(dir, TLS_MARKER_FILE);
|
|
1525
1607
|
if (enabled2) {
|
|
1526
|
-
|
|
1608
|
+
fs4.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1527
1609
|
} else {
|
|
1528
1610
|
try {
|
|
1529
|
-
|
|
1611
|
+
fs4.unlinkSync(markerPath);
|
|
1530
1612
|
} catch {
|
|
1531
1613
|
}
|
|
1532
1614
|
}
|
|
1533
1615
|
}
|
|
1534
1616
|
function readCustomCertMarker(dir) {
|
|
1535
1617
|
try {
|
|
1536
|
-
return
|
|
1618
|
+
return fs4.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
|
|
1537
1619
|
} catch {
|
|
1538
1620
|
return false;
|
|
1539
1621
|
}
|
|
@@ -1541,10 +1623,10 @@ function readCustomCertMarker(dir) {
|
|
|
1541
1623
|
function writeCustomCertMarker(dir, enabled2) {
|
|
1542
1624
|
const markerPath = path3.join(dir, CUSTOM_CERT_MARKER_FILE);
|
|
1543
1625
|
if (enabled2) {
|
|
1544
|
-
|
|
1626
|
+
fs4.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1545
1627
|
} else {
|
|
1546
1628
|
try {
|
|
1547
|
-
|
|
1629
|
+
fs4.unlinkSync(markerPath);
|
|
1548
1630
|
} catch {
|
|
1549
1631
|
}
|
|
1550
1632
|
}
|
|
@@ -1552,7 +1634,7 @@ function writeCustomCertMarker(dir, enabled2) {
|
|
|
1552
1634
|
var LAN_MARKER_FILE = "proxy.lan";
|
|
1553
1635
|
function readLanMarker(dir) {
|
|
1554
1636
|
try {
|
|
1555
|
-
const raw =
|
|
1637
|
+
const raw = fs4.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
|
|
1556
1638
|
return raw || null;
|
|
1557
1639
|
} catch {
|
|
1558
1640
|
return null;
|
|
@@ -1562,11 +1644,11 @@ function writeLanMarker(dir, ip) {
|
|
|
1562
1644
|
const markerPath = path3.join(dir, LAN_MARKER_FILE);
|
|
1563
1645
|
if (!ip) {
|
|
1564
1646
|
try {
|
|
1565
|
-
|
|
1647
|
+
fs4.unlinkSync(markerPath);
|
|
1566
1648
|
} catch {
|
|
1567
1649
|
}
|
|
1568
1650
|
} else {
|
|
1569
|
-
|
|
1651
|
+
fs4.writeFileSync(markerPath, ip, { mode: 420 });
|
|
1570
1652
|
}
|
|
1571
1653
|
}
|
|
1572
1654
|
var DEFAULT_TLD = "localhost";
|
|
@@ -1610,7 +1692,7 @@ var TLD_FILE = "proxy.tld";
|
|
|
1610
1692
|
var TLDS_FILE = "proxy.tlds";
|
|
1611
1693
|
function readLegacyTldFromDir(dir) {
|
|
1612
1694
|
try {
|
|
1613
|
-
const raw =
|
|
1695
|
+
const raw = fs4.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
|
|
1614
1696
|
return raw || DEFAULT_TLD;
|
|
1615
1697
|
} catch {
|
|
1616
1698
|
return DEFAULT_TLD;
|
|
@@ -1618,7 +1700,7 @@ function readLegacyTldFromDir(dir) {
|
|
|
1618
1700
|
}
|
|
1619
1701
|
function readTldsFromDir(dir) {
|
|
1620
1702
|
try {
|
|
1621
|
-
const raw =
|
|
1703
|
+
const raw = fs4.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
|
|
1622
1704
|
const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
|
|
1623
1705
|
if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
|
|
1624
1706
|
const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
|
|
@@ -1633,16 +1715,16 @@ function writeTldsFile(dir, tlds) {
|
|
|
1633
1715
|
const tldPath = path3.join(dir, TLD_FILE);
|
|
1634
1716
|
if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
|
|
1635
1717
|
try {
|
|
1636
|
-
|
|
1718
|
+
fs4.unlinkSync(tldsPath);
|
|
1637
1719
|
} catch {
|
|
1638
1720
|
}
|
|
1639
1721
|
try {
|
|
1640
|
-
|
|
1722
|
+
fs4.unlinkSync(tldPath);
|
|
1641
1723
|
} catch {
|
|
1642
1724
|
}
|
|
1643
1725
|
} else {
|
|
1644
|
-
|
|
1645
|
-
|
|
1726
|
+
fs4.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
|
|
1727
|
+
fs4.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
|
|
1646
1728
|
}
|
|
1647
1729
|
}
|
|
1648
1730
|
function writeTldFile(dir, tld) {
|
|
@@ -1957,7 +2039,7 @@ function collectBinPaths(cwd) {
|
|
|
1957
2039
|
let dir = cwd;
|
|
1958
2040
|
for (; ; ) {
|
|
1959
2041
|
const bin = path3.join(dir, "node_modules", ".bin");
|
|
1960
|
-
if (
|
|
2042
|
+
if (fs4.existsSync(bin)) {
|
|
1961
2043
|
dirs.push(bin);
|
|
1962
2044
|
}
|
|
1963
2045
|
const parent = path3.dirname(dir);
|
|
@@ -2088,7 +2170,7 @@ function injectFrameworkFlags(commandArgs, port) {
|
|
|
2088
2170
|
}
|
|
2089
2171
|
|
|
2090
2172
|
// src/clean-utils.ts
|
|
2091
|
-
import * as
|
|
2173
|
+
import * as fs5 from "fs";
|
|
2092
2174
|
import * as path4 from "path";
|
|
2093
2175
|
var PORTLESS_STATE_FILES = [
|
|
2094
2176
|
"routes.json",
|
|
@@ -2101,6 +2183,8 @@ var PORTLESS_STATE_FILES = [
|
|
|
2101
2183
|
"proxy.tld",
|
|
2102
2184
|
"proxy.tlds",
|
|
2103
2185
|
"proxy.lan",
|
|
2186
|
+
"ca.trusted",
|
|
2187
|
+
"ca.trust-refresh-pending",
|
|
2104
2188
|
"ca-key.pem",
|
|
2105
2189
|
"ca.pem",
|
|
2106
2190
|
"server-key.pem",
|
|
@@ -2110,28 +2194,38 @@ var PORTLESS_STATE_FILES = [
|
|
|
2110
2194
|
"ca.srl"
|
|
2111
2195
|
];
|
|
2112
2196
|
var HOST_CERTS_DIR2 = "host-certs";
|
|
2197
|
+
var CA_IDENTITY_FILES = /* @__PURE__ */ new Set(["ca-key.pem", "ca.pem", "ca.trust-refresh-pending"]);
|
|
2113
2198
|
function collectStateDirsForCleanup() {
|
|
2114
2199
|
const dirs = /* @__PURE__ */ new Set();
|
|
2115
2200
|
const add = (d) => {
|
|
2116
2201
|
const trimmed = d?.trim();
|
|
2117
2202
|
if (!trimmed) return;
|
|
2118
2203
|
const resolved = path4.resolve(trimmed);
|
|
2119
|
-
if (
|
|
2204
|
+
if (fs5.existsSync(resolved)) dirs.add(resolved);
|
|
2120
2205
|
};
|
|
2121
2206
|
add(USER_STATE_DIR);
|
|
2122
2207
|
add(LEGACY_SYSTEM_STATE_DIR);
|
|
2123
2208
|
add(process.env.PORTLESS_STATE_DIR);
|
|
2124
2209
|
return [...dirs];
|
|
2125
2210
|
}
|
|
2126
|
-
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 = {}) {
|
|
2127
2220
|
for (const f of PORTLESS_STATE_FILES) {
|
|
2221
|
+
if (options.preserveCAIdentity && CA_IDENTITY_FILES.has(f)) continue;
|
|
2128
2222
|
try {
|
|
2129
|
-
|
|
2223
|
+
fs5.unlinkSync(path4.join(dir, f));
|
|
2130
2224
|
} catch {
|
|
2131
2225
|
}
|
|
2132
2226
|
}
|
|
2133
2227
|
try {
|
|
2134
|
-
|
|
2228
|
+
fs5.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
2135
2229
|
} catch {
|
|
2136
2230
|
}
|
|
2137
2231
|
}
|
|
@@ -2340,7 +2434,7 @@ function cleanupAll() {
|
|
|
2340
2434
|
}
|
|
2341
2435
|
|
|
2342
2436
|
// src/config.ts
|
|
2343
|
-
import * as
|
|
2437
|
+
import * as fs6 from "fs";
|
|
2344
2438
|
import * as path5 from "path";
|
|
2345
2439
|
var ConfigValidationError = class extends Error {
|
|
2346
2440
|
constructor(message) {
|
|
@@ -2352,7 +2446,7 @@ var CONFIG_FILENAME = "portless.json";
|
|
|
2352
2446
|
function loadConfig(cwd = process.cwd()) {
|
|
2353
2447
|
const configPath = path5.join(cwd, CONFIG_FILENAME);
|
|
2354
2448
|
try {
|
|
2355
|
-
const raw =
|
|
2449
|
+
const raw = fs6.readFileSync(configPath, "utf-8");
|
|
2356
2450
|
const parsed = JSON.parse(raw);
|
|
2357
2451
|
validateConfig(parsed, configPath);
|
|
2358
2452
|
return { config: parsed, configDir: cwd };
|
|
@@ -2375,7 +2469,7 @@ function normalizePortlessValue(value) {
|
|
|
2375
2469
|
function loadConfigFromPackageJson(dir) {
|
|
2376
2470
|
const pkgPath = path5.join(dir, "package.json");
|
|
2377
2471
|
try {
|
|
2378
|
-
const raw =
|
|
2472
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2379
2473
|
const pkg = JSON.parse(raw);
|
|
2380
2474
|
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
2381
2475
|
const config = normalizePortlessValue(pkg.portless);
|
|
@@ -2393,7 +2487,7 @@ function loadConfigFromPackageJson(dir) {
|
|
|
2393
2487
|
function loadPackagePortlessConfig(dir) {
|
|
2394
2488
|
const pkgPath = path5.join(dir, "package.json");
|
|
2395
2489
|
try {
|
|
2396
|
-
const raw =
|
|
2490
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2397
2491
|
const pkg = JSON.parse(raw);
|
|
2398
2492
|
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
2399
2493
|
const config = normalizePortlessValue(pkg.portless);
|
|
@@ -2429,7 +2523,7 @@ function resolveAppConfig(config, configDir, packageDir) {
|
|
|
2429
2523
|
function hasScript(scriptName, dir) {
|
|
2430
2524
|
const pkgPath = path5.join(dir, "package.json");
|
|
2431
2525
|
try {
|
|
2432
|
-
const raw =
|
|
2526
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2433
2527
|
const pkg = JSON.parse(raw);
|
|
2434
2528
|
return typeof pkg?.scripts?.[scriptName] === "string";
|
|
2435
2529
|
} catch {
|
|
@@ -2448,7 +2542,7 @@ function detectPackageManager(cwd) {
|
|
|
2448
2542
|
for (; ; ) {
|
|
2449
2543
|
const pkgPath = path5.join(dir, "package.json");
|
|
2450
2544
|
try {
|
|
2451
|
-
const raw =
|
|
2545
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2452
2546
|
const pkg = JSON.parse(raw);
|
|
2453
2547
|
if (typeof pkg.packageManager === "string") {
|
|
2454
2548
|
const name = pkg.packageManager.split("@")[0];
|
|
@@ -2460,7 +2554,7 @@ function detectPackageManager(cwd) {
|
|
|
2460
2554
|
}
|
|
2461
2555
|
for (const [file, pm] of LOCK_FILES) {
|
|
2462
2556
|
try {
|
|
2463
|
-
|
|
2557
|
+
fs6.accessSync(path5.join(dir, file), fs6.constants.F_OK);
|
|
2464
2558
|
return pm;
|
|
2465
2559
|
} catch {
|
|
2466
2560
|
}
|
|
@@ -2619,13 +2713,13 @@ function warnUnknownKeys(obj, known, configPath, prefix) {
|
|
|
2619
2713
|
}
|
|
2620
2714
|
|
|
2621
2715
|
// src/workspace.ts
|
|
2622
|
-
import * as
|
|
2716
|
+
import * as fs7 from "fs";
|
|
2623
2717
|
import * as path6 from "path";
|
|
2624
2718
|
function findWorkspaceRoot(cwd = process.cwd()) {
|
|
2625
2719
|
let dir = cwd;
|
|
2626
2720
|
for (; ; ) {
|
|
2627
2721
|
try {
|
|
2628
|
-
|
|
2722
|
+
fs7.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs7.constants.R_OK);
|
|
2629
2723
|
return dir;
|
|
2630
2724
|
} catch {
|
|
2631
2725
|
}
|
|
@@ -2639,7 +2733,7 @@ function findWorkspaceRoot(cwd = process.cwd()) {
|
|
|
2639
2733
|
}
|
|
2640
2734
|
function detectWorkspaceSource(workspaceRoot) {
|
|
2641
2735
|
try {
|
|
2642
|
-
|
|
2736
|
+
fs7.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs7.constants.R_OK);
|
|
2643
2737
|
return "pnpm";
|
|
2644
2738
|
} catch {
|
|
2645
2739
|
}
|
|
@@ -2651,7 +2745,7 @@ function detectWorkspaceSource(workspaceRoot) {
|
|
|
2651
2745
|
function readWorkspacesFromPackageJson(dir) {
|
|
2652
2746
|
const pkgPath = path6.join(dir, "package.json");
|
|
2653
2747
|
try {
|
|
2654
|
-
const raw =
|
|
2748
|
+
const raw = fs7.readFileSync(pkgPath, "utf-8");
|
|
2655
2749
|
const pkg = JSON.parse(raw);
|
|
2656
2750
|
if (!pkg || typeof pkg !== "object") return null;
|
|
2657
2751
|
const ws = pkg.workspaces;
|
|
@@ -2672,7 +2766,7 @@ function discoverWorkspacePackages(workspaceRoot) {
|
|
|
2672
2766
|
const wsPath = path6.join(workspaceRoot, "pnpm-workspace.yaml");
|
|
2673
2767
|
let content;
|
|
2674
2768
|
try {
|
|
2675
|
-
content =
|
|
2769
|
+
content = fs7.readFileSync(wsPath, "utf-8");
|
|
2676
2770
|
} catch {
|
|
2677
2771
|
return [];
|
|
2678
2772
|
}
|
|
@@ -2687,7 +2781,7 @@ function discoverWorkspacePackages(workspaceRoot) {
|
|
|
2687
2781
|
for (const dir of dirs) {
|
|
2688
2782
|
const pkgPath = path6.join(dir, "package.json");
|
|
2689
2783
|
try {
|
|
2690
|
-
const raw =
|
|
2784
|
+
const raw = fs7.readFileSync(pkgPath, "utf-8");
|
|
2691
2785
|
const pkg = JSON.parse(raw);
|
|
2692
2786
|
const rawName = typeof pkg.name === "string" ? pkg.name : null;
|
|
2693
2787
|
const scopeMatch = rawName?.match(/^@([^/]+)\//);
|
|
@@ -2769,7 +2863,7 @@ function expandSingleGlob(root, glob) {
|
|
|
2769
2863
|
function expandSegments(base, segments) {
|
|
2770
2864
|
if (segments.length === 0) {
|
|
2771
2865
|
try {
|
|
2772
|
-
const stat =
|
|
2866
|
+
const stat = fs7.statSync(base);
|
|
2773
2867
|
if (stat.isDirectory()) return [base];
|
|
2774
2868
|
} catch {
|
|
2775
2869
|
}
|
|
@@ -2778,7 +2872,7 @@ function expandSegments(base, segments) {
|
|
|
2778
2872
|
const [current, ...rest] = segments;
|
|
2779
2873
|
if (current.includes("*")) {
|
|
2780
2874
|
try {
|
|
2781
|
-
const entries =
|
|
2875
|
+
const entries = fs7.readdirSync(base, { withFileTypes: true });
|
|
2782
2876
|
const matched = entries.filter((e) => e.isDirectory() && segmentMatches(current, e.name));
|
|
2783
2877
|
if (rest.length === 0) {
|
|
2784
2878
|
return matched.map((e) => path6.join(base, e.name));
|
|
@@ -2796,7 +2890,7 @@ function expandSegments(base, segments) {
|
|
|
2796
2890
|
}
|
|
2797
2891
|
|
|
2798
2892
|
// src/turbo.ts
|
|
2799
|
-
import * as
|
|
2893
|
+
import * as fs8 from "fs";
|
|
2800
2894
|
import * as path7 from "path";
|
|
2801
2895
|
var LOADER_FILENAME = "turbo-env-loader.cjs";
|
|
2802
2896
|
var MANIFEST_FILENAME = "dev-manifest.json";
|
|
@@ -2826,23 +2920,23 @@ try {
|
|
|
2826
2920
|
`;
|
|
2827
2921
|
}
|
|
2828
2922
|
function ensureEnvLoader(baseDir = USER_STATE_DIR) {
|
|
2829
|
-
|
|
2923
|
+
fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2830
2924
|
const target = loaderPath(baseDir);
|
|
2831
2925
|
const source = loaderSource(baseDir);
|
|
2832
2926
|
try {
|
|
2833
|
-
const existing =
|
|
2927
|
+
const existing = fs8.readFileSync(target, "utf-8");
|
|
2834
2928
|
if (existing === source) return;
|
|
2835
2929
|
} catch {
|
|
2836
2930
|
}
|
|
2837
|
-
|
|
2931
|
+
fs8.writeFileSync(target, source, { mode: 420 });
|
|
2838
2932
|
}
|
|
2839
2933
|
function writeManifest(entries, baseDir = USER_STATE_DIR) {
|
|
2840
|
-
|
|
2841
|
-
|
|
2934
|
+
fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2935
|
+
fs8.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
|
|
2842
2936
|
}
|
|
2843
2937
|
function removeManifest(baseDir = USER_STATE_DIR) {
|
|
2844
2938
|
try {
|
|
2845
|
-
|
|
2939
|
+
fs8.unlinkSync(manifestPath(baseDir));
|
|
2846
2940
|
} catch {
|
|
2847
2941
|
}
|
|
2848
2942
|
}
|
|
@@ -2854,7 +2948,7 @@ function buildNodeOptions(baseDir = USER_STATE_DIR) {
|
|
|
2854
2948
|
}
|
|
2855
2949
|
function hasTurboConfig(wsRoot) {
|
|
2856
2950
|
try {
|
|
2857
|
-
|
|
2951
|
+
fs8.accessSync(path7.join(wsRoot, "turbo.json"), fs8.constants.R_OK);
|
|
2858
2952
|
return true;
|
|
2859
2953
|
} catch {
|
|
2860
2954
|
return false;
|
|
@@ -2862,8 +2956,8 @@ function hasTurboConfig(wsRoot) {
|
|
|
2862
2956
|
}
|
|
2863
2957
|
|
|
2864
2958
|
// src/service.ts
|
|
2865
|
-
import * as
|
|
2866
|
-
import * as
|
|
2959
|
+
import * as fs9 from "fs";
|
|
2960
|
+
import * as os3 from "os";
|
|
2867
2961
|
import * as path8 from "path";
|
|
2868
2962
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2869
2963
|
var DEFAULT_SERVICE_PORT = getProtocolPort(true);
|
|
@@ -2894,7 +2988,7 @@ var DEFAULT_SERVICE_CONFIG = {
|
|
|
2894
2988
|
useWildcard: false,
|
|
2895
2989
|
extraEnv: {}
|
|
2896
2990
|
};
|
|
2897
|
-
function
|
|
2991
|
+
function defaultRunner4(command, args, options) {
|
|
2898
2992
|
return spawnSync4(command, args, {
|
|
2899
2993
|
encoding: "utf-8",
|
|
2900
2994
|
stdio: options?.stdio ?? "pipe"
|
|
@@ -2936,7 +3030,7 @@ function getFlagValue(args, index, flag) {
|
|
|
2936
3030
|
return value;
|
|
2937
3031
|
}
|
|
2938
3032
|
function resolveServicePath(value) {
|
|
2939
|
-
const expanded = value === "~" ?
|
|
3033
|
+
const expanded = value === "~" ? os3.homedir() : value.startsWith("~/") || value.startsWith("~\\") ? path8.join(os3.homedir(), value.slice(2)) : value;
|
|
2940
3034
|
return path8.resolve(expanded);
|
|
2941
3035
|
}
|
|
2942
3036
|
function normalizeServiceInstallPaths(config) {
|
|
@@ -3064,34 +3158,21 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
3064
3158
|
}
|
|
3065
3159
|
return config;
|
|
3066
3160
|
}
|
|
3067
|
-
function readPasswdHome(username) {
|
|
3068
|
-
try {
|
|
3069
|
-
const passwd = fs8.readFileSync("/etc/passwd", "utf-8");
|
|
3070
|
-
for (const line of passwd.split("\n")) {
|
|
3071
|
-
const fields = line.split(":");
|
|
3072
|
-
if (fields[0] === username && fields[5]) {
|
|
3073
|
-
return fields[5];
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
} catch {
|
|
3077
|
-
}
|
|
3078
|
-
return null;
|
|
3079
|
-
}
|
|
3080
3161
|
function resolveUserContext(platform) {
|
|
3081
3162
|
if (platform === "win32") {
|
|
3082
|
-
const home =
|
|
3163
|
+
const home = resolveUserHome({ platform });
|
|
3083
3164
|
return { home, username: process.env.USERNAME };
|
|
3084
3165
|
}
|
|
3085
3166
|
const sudoUser = process.env.SUDO_USER;
|
|
3086
3167
|
const sudoUid = process.env.SUDO_UID;
|
|
3087
3168
|
const sudoGid = process.env.SUDO_GID;
|
|
3088
3169
|
if (sudoUser && sudoUser !== "root") {
|
|
3089
|
-
const home =
|
|
3170
|
+
const home = resolveUserHome({ platform });
|
|
3090
3171
|
return { home, uid: sudoUid, gid: sudoGid, username: sudoUser };
|
|
3091
3172
|
}
|
|
3092
|
-
const userInfo2 =
|
|
3173
|
+
const userInfo2 = os3.userInfo();
|
|
3093
3174
|
return {
|
|
3094
|
-
home:
|
|
3175
|
+
home: os3.homedir(),
|
|
3095
3176
|
uid: process.getuid?.()?.toString(),
|
|
3096
3177
|
gid: process.getgid?.()?.toString(),
|
|
3097
3178
|
username: userInfo2.username
|
|
@@ -3369,8 +3450,8 @@ function parsePlistEnv(block) {
|
|
|
3369
3450
|
function readInstalledServiceSnapshot(spec) {
|
|
3370
3451
|
try {
|
|
3371
3452
|
if (spec.platform === "darwin") {
|
|
3372
|
-
if (!
|
|
3373
|
-
const plist =
|
|
3453
|
+
if (!fs9.existsSync(spec.plistPath)) return null;
|
|
3454
|
+
const plist = fs9.readFileSync(spec.plistPath, "utf-8");
|
|
3374
3455
|
const argsBlock = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
3375
3456
|
const envBlock = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
|
|
3376
3457
|
if (!argsBlock) return null;
|
|
@@ -3380,8 +3461,8 @@ function readInstalledServiceSnapshot(spec) {
|
|
|
3380
3461
|
};
|
|
3381
3462
|
}
|
|
3382
3463
|
if (spec.platform === "linux") {
|
|
3383
|
-
if (!
|
|
3384
|
-
const unit =
|
|
3464
|
+
if (!fs9.existsSync(spec.unitPath)) return null;
|
|
3465
|
+
const unit = fs9.readFileSync(spec.unitPath, "utf-8");
|
|
3385
3466
|
const env2 = {};
|
|
3386
3467
|
let command = null;
|
|
3387
3468
|
for (const line of unit.split("\n")) {
|
|
@@ -3399,8 +3480,8 @@ function readInstalledServiceSnapshot(spec) {
|
|
|
3399
3480
|
}
|
|
3400
3481
|
return command ? { command, env: env2 } : null;
|
|
3401
3482
|
}
|
|
3402
|
-
if (!
|
|
3403
|
-
const script =
|
|
3483
|
+
if (!fs9.existsSync(spec.scriptPath)) return null;
|
|
3484
|
+
const script = fs9.readFileSync(spec.scriptPath, "utf-8");
|
|
3404
3485
|
const env = {};
|
|
3405
3486
|
let commandLine = null;
|
|
3406
3487
|
for (const rawLine of script.split(/\r?\n/)) {
|
|
@@ -3462,7 +3543,7 @@ function buildElevatedEnvArgs(options) {
|
|
|
3462
3543
|
}
|
|
3463
3544
|
function buildServiceUninstallSudoArgs(entryScript, options = {}) {
|
|
3464
3545
|
const env = options.env ?? process.env;
|
|
3465
|
-
const home = options.home ??
|
|
3546
|
+
const home = options.home ?? os3.homedir();
|
|
3466
3547
|
const stateDir = options.stateDir ?? env.PORTLESS_STATE_DIR ?? path8.join(home, ".portless");
|
|
3467
3548
|
return [
|
|
3468
3549
|
...buildElevatedEnvArgs({ home, stateDir, env }),
|
|
@@ -3476,7 +3557,7 @@ function requireUnixElevation(args, runner) {
|
|
|
3476
3557
|
if (process.platform !== "darwin" && process.platform !== "linux") return;
|
|
3477
3558
|
if ((process.getuid?.() ?? -1) === 0) return;
|
|
3478
3559
|
if (process.env[INTERNAL_ELEVATED_ENV] === "1") return;
|
|
3479
|
-
const home =
|
|
3560
|
+
const home = os3.homedir();
|
|
3480
3561
|
const stateDir = process.env.PORTLESS_STATE_DIR || path8.join(home, ".portless");
|
|
3481
3562
|
const result = runner(
|
|
3482
3563
|
"sudo",
|
|
@@ -3533,7 +3614,7 @@ async function stopExistingProxy(entryScript, runner, proxyPort) {
|
|
|
3533
3614
|
}
|
|
3534
3615
|
}
|
|
3535
3616
|
function prepareServiceState(stateDir) {
|
|
3536
|
-
|
|
3617
|
+
fs9.mkdirSync(stateDir, { recursive: true });
|
|
3537
3618
|
fixOwnership(stateDir);
|
|
3538
3619
|
}
|
|
3539
3620
|
function prepareTrust(stateDir) {
|
|
@@ -3581,8 +3662,8 @@ async function installService(entryScript, runner, args) {
|
|
|
3581
3662
|
if (spec.platform === "darwin") {
|
|
3582
3663
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3583
3664
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3584
|
-
|
|
3585
|
-
|
|
3665
|
+
fs9.writeFileSync(spec.plistPath, spec.plist);
|
|
3666
|
+
fs9.chmodSync(spec.plistPath, 420);
|
|
3586
3667
|
runRequired(runner, "chown", ["root:wheel", spec.plistPath]);
|
|
3587
3668
|
runRequired(runner, "launchctl", ["bootstrap", "system", spec.plistPath]);
|
|
3588
3669
|
runRequired(runner, "launchctl", ["enable", `system/${spec.label}`]);
|
|
@@ -3590,16 +3671,16 @@ async function installService(entryScript, runner, args) {
|
|
|
3590
3671
|
} else if (spec.platform === "linux") {
|
|
3591
3672
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3592
3673
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3593
|
-
|
|
3594
|
-
|
|
3674
|
+
fs9.writeFileSync(spec.unitPath, spec.unit);
|
|
3675
|
+
fs9.chmodSync(spec.unitPath, 420);
|
|
3595
3676
|
runRequired(runner, "systemctl", ["daemon-reload"]);
|
|
3596
3677
|
runRequired(runner, "systemctl", ["enable", spec.serviceName]);
|
|
3597
3678
|
runRequired(runner, "systemctl", ["restart", spec.serviceName]);
|
|
3598
3679
|
} else {
|
|
3599
3680
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3600
3681
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3601
|
-
|
|
3602
|
-
|
|
3682
|
+
fs9.mkdirSync(spec.scriptDir, { recursive: true });
|
|
3683
|
+
fs9.writeFileSync(spec.scriptPath, spec.script);
|
|
3603
3684
|
runRequired(runner, "schtasks", spec.createArgs);
|
|
3604
3685
|
runOptional(runner, "schtasks", spec.runArgs);
|
|
3605
3686
|
}
|
|
@@ -3612,32 +3693,32 @@ async function uninstallService(entryScript, runner) {
|
|
|
3612
3693
|
const spec = currentServiceSpec(entryScript);
|
|
3613
3694
|
if (spec.platform === "darwin") {
|
|
3614
3695
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3615
|
-
|
|
3696
|
+
fs9.rmSync(spec.plistPath, { force: true });
|
|
3616
3697
|
} else if (spec.platform === "linux") {
|
|
3617
3698
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3618
|
-
|
|
3699
|
+
fs9.rmSync(spec.unitPath, { force: true });
|
|
3619
3700
|
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3620
3701
|
} else {
|
|
3621
3702
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3622
3703
|
runOptional(runner, "schtasks", spec.deleteArgs);
|
|
3623
|
-
|
|
3704
|
+
fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3624
3705
|
}
|
|
3625
3706
|
console.log(colors_default.green("Portless service uninstalled."));
|
|
3626
3707
|
}
|
|
3627
|
-
function tryUninstallService(entryScript, runner =
|
|
3708
|
+
function tryUninstallService(entryScript, runner = defaultRunner4) {
|
|
3628
3709
|
let installed = false;
|
|
3629
3710
|
try {
|
|
3630
3711
|
const spec = currentServiceSpec(entryScript);
|
|
3631
3712
|
if (spec.platform === "darwin") {
|
|
3632
|
-
installed =
|
|
3713
|
+
installed = fs9.existsSync(spec.plistPath);
|
|
3633
3714
|
if (!installed) return { removed: false, installed: false };
|
|
3634
3715
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3635
|
-
|
|
3716
|
+
fs9.rmSync(spec.plistPath, { force: true });
|
|
3636
3717
|
} else if (spec.platform === "linux") {
|
|
3637
|
-
installed =
|
|
3718
|
+
installed = fs9.existsSync(spec.unitPath);
|
|
3638
3719
|
if (!installed) return { removed: false, installed: false };
|
|
3639
3720
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3640
|
-
|
|
3721
|
+
fs9.rmSync(spec.unitPath, { force: true });
|
|
3641
3722
|
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3642
3723
|
} else {
|
|
3643
3724
|
const query = runner("schtasks", ["/Query", "/TN", spec.taskName, "/FO", "LIST"]);
|
|
@@ -3645,7 +3726,7 @@ function tryUninstallService(entryScript, runner = defaultRunner3) {
|
|
|
3645
3726
|
if (!installed) return { removed: false, installed: false };
|
|
3646
3727
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3647
3728
|
runRequired(runner, "schtasks", spec.deleteArgs);
|
|
3648
|
-
|
|
3729
|
+
fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3649
3730
|
}
|
|
3650
3731
|
return { removed: true, installed: true };
|
|
3651
3732
|
} catch (err) {
|
|
@@ -3662,7 +3743,7 @@ async function getServiceStatus(entryScript, runner) {
|
|
|
3662
3743
|
const installedConfig = readInstalledServiceConfig(spec) ?? spec.config;
|
|
3663
3744
|
const proxyRunning = await isProxyRunning(installedConfig.proxyPort, installedConfig.useHttps);
|
|
3664
3745
|
if (spec.platform === "darwin") {
|
|
3665
|
-
const installed2 =
|
|
3746
|
+
const installed2 = fs9.existsSync(spec.plistPath);
|
|
3666
3747
|
const result = runner("launchctl", ["print", `system/${spec.label}`]);
|
|
3667
3748
|
const output2 = `${result.stdout || ""}${result.stderr || ""}`;
|
|
3668
3749
|
const managerState = result.status === 0 && /state = running|pid = \d+/.test(output2) ? "running" : installed2 ? "installed" : "not installed";
|
|
@@ -3677,7 +3758,7 @@ async function getServiceStatus(entryScript, runner) {
|
|
|
3677
3758
|
if (spec.platform === "linux") {
|
|
3678
3759
|
const enabled2 = runner("systemctl", ["is-enabled", spec.serviceName]);
|
|
3679
3760
|
const active = runner("systemctl", ["is-active", spec.serviceName]);
|
|
3680
|
-
const installed2 = enabled2.status === 0 || active.status === 0 ||
|
|
3761
|
+
const installed2 = enabled2.status === 0 || active.status === 0 || fs9.existsSync(spec.unitPath);
|
|
3681
3762
|
const activeText = (active.stdout || "").trim();
|
|
3682
3763
|
return {
|
|
3683
3764
|
installed: installed2,
|
|
@@ -3752,7 +3833,7 @@ ${colors_default.bold("Notes:")}
|
|
|
3752
3833
|
}
|
|
3753
3834
|
async function handleService(args, options) {
|
|
3754
3835
|
const action = args[1];
|
|
3755
|
-
const runner = options.runner ||
|
|
3836
|
+
const runner = options.runner || defaultRunner4;
|
|
3756
3837
|
if (!action || action === "--help" || action === "-h") {
|
|
3757
3838
|
printServiceHelp();
|
|
3758
3839
|
process.exit(0);
|
|
@@ -3949,7 +4030,7 @@ function getEntryScript() {
|
|
|
3949
4030
|
function isLocallyInstalled() {
|
|
3950
4031
|
let dir = process.cwd();
|
|
3951
4032
|
for (; ; ) {
|
|
3952
|
-
if (
|
|
4033
|
+
if (fs10.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
|
|
3953
4034
|
return true;
|
|
3954
4035
|
}
|
|
3955
4036
|
const parent = path9.dirname(dir);
|
|
@@ -4063,11 +4144,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4063
4144
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
4064
4145
|
}
|
|
4065
4146
|
const routesPath = store.getRoutesPath();
|
|
4066
|
-
if (!
|
|
4067
|
-
|
|
4147
|
+
if (!fs10.existsSync(routesPath)) {
|
|
4148
|
+
fs10.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
4068
4149
|
}
|
|
4069
4150
|
try {
|
|
4070
|
-
|
|
4151
|
+
fs10.chmodSync(routesPath, FILE_MODE);
|
|
4071
4152
|
} catch {
|
|
4072
4153
|
}
|
|
4073
4154
|
fixOwnership(routesPath);
|
|
@@ -4127,7 +4208,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4127
4208
|
}
|
|
4128
4209
|
};
|
|
4129
4210
|
try {
|
|
4130
|
-
watcher =
|
|
4211
|
+
watcher = fs10.watch(routesPath, () => {
|
|
4131
4212
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
4132
4213
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
4133
4214
|
});
|
|
@@ -4178,8 +4259,8 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4178
4259
|
redirectServer.listen(80);
|
|
4179
4260
|
}
|
|
4180
4261
|
server.listen(proxyPort, () => {
|
|
4181
|
-
|
|
4182
|
-
|
|
4262
|
+
fs10.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
4263
|
+
fs10.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
4183
4264
|
writeTlsMarker(store.dir, isTls);
|
|
4184
4265
|
writeCustomCertMarker(store.dir, isTls && customCert);
|
|
4185
4266
|
writeTldsFile(store.dir, tlds);
|
|
@@ -4228,11 +4309,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4228
4309
|
redirectServer.close();
|
|
4229
4310
|
}
|
|
4230
4311
|
try {
|
|
4231
|
-
|
|
4312
|
+
fs10.unlinkSync(store.pidPath);
|
|
4232
4313
|
} catch {
|
|
4233
4314
|
}
|
|
4234
4315
|
try {
|
|
4235
|
-
|
|
4316
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4236
4317
|
} catch {
|
|
4237
4318
|
}
|
|
4238
4319
|
writeTlsMarker(store.dir, false);
|
|
@@ -4263,7 +4344,7 @@ function sudoStopOrHint(port) {
|
|
|
4263
4344
|
}
|
|
4264
4345
|
async function stopProxy(store, proxyPort, _tls) {
|
|
4265
4346
|
const pidPath = store.pidPath;
|
|
4266
|
-
if (!
|
|
4347
|
+
if (!fs10.existsSync(pidPath)) {
|
|
4267
4348
|
if (await isProxyRunning(proxyPort)) {
|
|
4268
4349
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
4269
4350
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -4271,7 +4352,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4271
4352
|
try {
|
|
4272
4353
|
process.kill(pid, "SIGTERM");
|
|
4273
4354
|
try {
|
|
4274
|
-
|
|
4355
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4275
4356
|
} catch {
|
|
4276
4357
|
}
|
|
4277
4358
|
writeTlsMarker(store.dir, false);
|
|
@@ -4310,10 +4391,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4310
4391
|
return;
|
|
4311
4392
|
}
|
|
4312
4393
|
try {
|
|
4313
|
-
const pid = parseInt(
|
|
4394
|
+
const pid = parseInt(fs10.readFileSync(pidPath, "utf-8"), 10);
|
|
4314
4395
|
if (isNaN(pid)) {
|
|
4315
4396
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
4316
|
-
|
|
4397
|
+
fs10.unlinkSync(pidPath);
|
|
4317
4398
|
writeTlsMarker(store.dir, false);
|
|
4318
4399
|
writeCustomCertMarker(store.dir, false);
|
|
4319
4400
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
@@ -4328,9 +4409,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4328
4409
|
return;
|
|
4329
4410
|
}
|
|
4330
4411
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
4331
|
-
|
|
4412
|
+
fs10.unlinkSync(pidPath);
|
|
4332
4413
|
try {
|
|
4333
|
-
|
|
4414
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4334
4415
|
} catch {
|
|
4335
4416
|
}
|
|
4336
4417
|
writeTlsMarker(store.dir, false);
|
|
@@ -4346,7 +4427,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4346
4427
|
)
|
|
4347
4428
|
);
|
|
4348
4429
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
4349
|
-
|
|
4430
|
+
fs10.unlinkSync(pidPath);
|
|
4350
4431
|
writeTlsMarker(store.dir, false);
|
|
4351
4432
|
writeCustomCertMarker(store.dir, false);
|
|
4352
4433
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
@@ -4354,9 +4435,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4354
4435
|
return;
|
|
4355
4436
|
}
|
|
4356
4437
|
process.kill(pid, "SIGTERM");
|
|
4357
|
-
|
|
4438
|
+
fs10.unlinkSync(pidPath);
|
|
4358
4439
|
try {
|
|
4359
|
-
|
|
4440
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4360
4441
|
} catch {
|
|
4361
4442
|
}
|
|
4362
4443
|
writeTlsMarker(store.dir, false);
|
|
@@ -4507,7 +4588,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4507
4588
|
const logPath = path9.join(fallbackDir, "proxy.log");
|
|
4508
4589
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
4509
4590
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
4510
|
-
if (
|
|
4591
|
+
if (fs10.existsSync(logPath)) {
|
|
4511
4592
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
4512
4593
|
}
|
|
4513
4594
|
process.exit(1);
|
|
@@ -4772,7 +4853,7 @@ portless
|
|
|
4772
4853
|
const caEnv = {};
|
|
4773
4854
|
if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
|
|
4774
4855
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
4775
|
-
if (
|
|
4856
|
+
if (fs10.existsSync(caPath)) {
|
|
4776
4857
|
caEnv.NODE_EXTRA_CA_CERTS = caPath;
|
|
4777
4858
|
}
|
|
4778
4859
|
}
|
|
@@ -5051,11 +5132,13 @@ ${colors_default.bold("How it works:")}
|
|
|
5051
5132
|
5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
|
|
5052
5133
|
Expo, React Native) get --port and, when needed, --host flags
|
|
5053
5134
|
injected automatically
|
|
5135
|
+
Elevated proxy processes keep the invoking user's ~/.portless state directory.
|
|
5054
5136
|
|
|
5055
5137
|
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
5056
5138
|
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
5057
5139
|
On first use, portless generates a local CA and adds it to your
|
|
5058
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.
|
|
5059
5142
|
|
|
5060
5143
|
${colors_default.bold("LAN mode:")}
|
|
5061
5144
|
Use --lan to make services accessible from other devices (phones,
|
|
@@ -5157,13 +5240,13 @@ ${colors_default.bold("Reserved names:")}
|
|
|
5157
5240
|
process.exit(0);
|
|
5158
5241
|
}
|
|
5159
5242
|
function printVersion() {
|
|
5160
|
-
console.log("0.15.
|
|
5243
|
+
console.log("0.15.3");
|
|
5161
5244
|
process.exit(0);
|
|
5162
5245
|
}
|
|
5163
5246
|
async function handleTrust() {
|
|
5164
5247
|
const { dir } = await discoverState();
|
|
5165
|
-
if (!
|
|
5166
|
-
|
|
5248
|
+
if (!fs10.existsSync(dir)) {
|
|
5249
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
5167
5250
|
}
|
|
5168
5251
|
const { caGenerated } = ensureCerts(dir);
|
|
5169
5252
|
if (caGenerated) {
|
|
@@ -5211,7 +5294,8 @@ directory, and PORTLESS_STATE_DIR when set), and removes the portless block
|
|
|
5211
5294
|
from ${HOSTS_DISPLAY}.
|
|
5212
5295
|
|
|
5213
5296
|
Only allowlisted filenames under each state directory are deleted. Custom
|
|
5214
|
-
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.
|
|
5215
5299
|
|
|
5216
5300
|
macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
|
|
5217
5301
|
require elevated privileges. On Windows, run as Administrator if needed.
|
|
@@ -5267,27 +5351,32 @@ ${colors_default.bold("Options:")}
|
|
|
5267
5351
|
}
|
|
5268
5352
|
}
|
|
5269
5353
|
const stateDirs = collectStateDirsForCleanup();
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
const wasTrusted = isCATrusted(stateDir);
|
|
5274
|
-
if (!wasTrusted) continue;
|
|
5275
|
-
const untrustResult = untrustCA(stateDir);
|
|
5354
|
+
const failedCAStateDirs = /* @__PURE__ */ new Set();
|
|
5355
|
+
const caRemovalResults = attemptCATrustRemovalForCleanup(stateDirs, untrustCA);
|
|
5356
|
+
for (const [stateDir, untrustResult] of caRemovalResults) {
|
|
5276
5357
|
if (untrustResult.removed) {
|
|
5277
5358
|
console.log(colors_default.green("Removed local CA from the system trust store."));
|
|
5278
|
-
} else
|
|
5359
|
+
} else {
|
|
5360
|
+
failedCAStateDirs.add(stateDir);
|
|
5279
5361
|
console.warn(
|
|
5280
5362
|
colors_default.yellow(
|
|
5281
|
-
`Could not remove CA from trust store: ${untrustResult.error}
|
|
5363
|
+
`Could not remove CA from trust store: ${untrustResult.error ?? "unknown error"}
|
|
5282
5364
|
Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
5283
5365
|
)
|
|
5284
5366
|
);
|
|
5285
5367
|
}
|
|
5286
5368
|
}
|
|
5287
5369
|
for (const stateDir of stateDirs) {
|
|
5288
|
-
removePortlessStateFiles(stateDir
|
|
5370
|
+
removePortlessStateFiles(stateDir, {
|
|
5371
|
+
preserveCAIdentity: failedCAStateDirs.has(stateDir)
|
|
5372
|
+
});
|
|
5289
5373
|
}
|
|
5290
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
|
+
}
|
|
5291
5380
|
if (cleanHostsFile()) {
|
|
5292
5381
|
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
5293
5382
|
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
@@ -5613,7 +5702,7 @@ function isProcessAliveForDoctor(pid) {
|
|
|
5613
5702
|
}
|
|
5614
5703
|
function checkPathWritable(targetPath) {
|
|
5615
5704
|
try {
|
|
5616
|
-
|
|
5705
|
+
fs10.accessSync(targetPath, fs10.constants.W_OK);
|
|
5617
5706
|
return true;
|
|
5618
5707
|
} catch {
|
|
5619
5708
|
return false;
|
|
@@ -5622,7 +5711,7 @@ function checkPathWritable(targetPath) {
|
|
|
5622
5711
|
function findExistingAncestor(targetPath) {
|
|
5623
5712
|
let current = targetPath;
|
|
5624
5713
|
for (; ; ) {
|
|
5625
|
-
if (
|
|
5714
|
+
if (fs10.existsSync(current)) return current;
|
|
5626
5715
|
const parent = path9.dirname(current);
|
|
5627
5716
|
if (parent === current) return null;
|
|
5628
5717
|
current = parent;
|
|
@@ -5693,7 +5782,7 @@ ${colors_default.bold("Options:")}
|
|
|
5693
5782
|
const store = new RouteStore(state.dir, {
|
|
5694
5783
|
onWarning: (msg) => add("warn", msg)
|
|
5695
5784
|
});
|
|
5696
|
-
const hasPortFile =
|
|
5785
|
+
const hasPortFile = fs10.existsSync(store.portFilePath);
|
|
5697
5786
|
const configuredTls = hasPortFile ? state.tls : !isHttpsEnvDisabled();
|
|
5698
5787
|
const configuredPort = hasPortFile ? state.port : getDefaultPort(configuredTls);
|
|
5699
5788
|
const initialProxyRunning = await isProxyRunning(state.port, state.tls);
|
|
@@ -5705,9 +5794,9 @@ ${colors_default.bold("Options:")}
|
|
|
5705
5794
|
const proxyTls = proxyRunning || portListening || hasPortFile ? probeTls : configuredTls;
|
|
5706
5795
|
const currentProxyStateIsHttp = (proxyRunning || portListening || hasPortFile) && !proxyTls;
|
|
5707
5796
|
const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
|
|
5708
|
-
const stateExists =
|
|
5797
|
+
const stateExists = fs10.existsSync(state.dir);
|
|
5709
5798
|
console.log(colors_default.blue.bold("\nportless doctor\n"));
|
|
5710
|
-
console.log(`Version: ${"0.15.
|
|
5799
|
+
console.log(`Version: ${"0.15.3"}`);
|
|
5711
5800
|
console.log(`Node.js: ${process.versions.node}`);
|
|
5712
5801
|
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
5713
5802
|
console.log(`State dir: ${state.dir}`);
|
|
@@ -5724,7 +5813,7 @@ ${colors_default.bold("Options:")}
|
|
|
5724
5813
|
}
|
|
5725
5814
|
if (stateExists) {
|
|
5726
5815
|
try {
|
|
5727
|
-
const stat =
|
|
5816
|
+
const stat = fs10.statSync(state.dir);
|
|
5728
5817
|
if (!stat.isDirectory()) {
|
|
5729
5818
|
add("fail", `State path exists but is not a directory: ${state.dir}`);
|
|
5730
5819
|
} else if (checkPathWritable(state.dir)) {
|
|
@@ -5744,7 +5833,7 @@ ${colors_default.bold("Options:")}
|
|
|
5744
5833
|
`State directory does not exist and no writable ancestor was found: ${state.dir}`
|
|
5745
5834
|
);
|
|
5746
5835
|
} else {
|
|
5747
|
-
const ancestorStat =
|
|
5836
|
+
const ancestorStat = fs10.statSync(ancestor);
|
|
5748
5837
|
if (!ancestorStat.isDirectory()) {
|
|
5749
5838
|
add("fail", `State directory does not exist and ancestor is not a directory: ${ancestor}`);
|
|
5750
5839
|
} else if (checkPathWritable(ancestor)) {
|
|
@@ -5770,9 +5859,9 @@ ${colors_default.bold("Options:")}
|
|
|
5770
5859
|
doctorProxyStartHint(proxyPort, proxyTls)
|
|
5771
5860
|
);
|
|
5772
5861
|
}
|
|
5773
|
-
if (
|
|
5862
|
+
if (fs10.existsSync(store.pidPath)) {
|
|
5774
5863
|
try {
|
|
5775
|
-
const rawPid =
|
|
5864
|
+
const rawPid = fs10.readFileSync(store.pidPath, "utf-8").trim();
|
|
5776
5865
|
const pid = parseInt(rawPid, 10);
|
|
5777
5866
|
if (isNaN(pid) || pid <= 0) {
|
|
5778
5867
|
add("fail", `Proxy PID file is invalid: ${store.pidPath}`);
|
|
@@ -5822,7 +5911,7 @@ ${colors_default.bold("Options:")}
|
|
|
5822
5911
|
add("info", "Generated local CA is not required for custom TLS certificates.");
|
|
5823
5912
|
} else if (proxyTls) {
|
|
5824
5913
|
const caPath = path9.join(state.dir, "ca.pem");
|
|
5825
|
-
if (
|
|
5914
|
+
if (fs10.existsSync(caPath)) {
|
|
5826
5915
|
if (isCATrusted(state.dir)) {
|
|
5827
5916
|
add("ok", "Local CA is trusted by the OS trust store.");
|
|
5828
5917
|
} else {
|
|
@@ -6247,7 +6336,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6247
6336
|
} else {
|
|
6248
6337
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
6249
6338
|
const logPath2 = path9.join(resolveStateDir(proxyPort), "proxy.log");
|
|
6250
|
-
if (
|
|
6339
|
+
if (fs10.existsSync(logPath2)) {
|
|
6251
6340
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
6252
6341
|
}
|
|
6253
6342
|
}
|
|
@@ -6286,8 +6375,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6286
6375
|
store.ensureDir();
|
|
6287
6376
|
if (customCertPath && customKeyPath) {
|
|
6288
6377
|
try {
|
|
6289
|
-
const cert =
|
|
6290
|
-
const key =
|
|
6378
|
+
const cert = fs10.readFileSync(customCertPath);
|
|
6379
|
+
const key = fs10.readFileSync(customKeyPath);
|
|
6291
6380
|
const certStr = cert.toString("utf-8");
|
|
6292
6381
|
const keyStr = key.toString("utf-8");
|
|
6293
6382
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -6332,9 +6421,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6332
6421
|
console.warn(colors_default.cyan(" portless trust"));
|
|
6333
6422
|
}
|
|
6334
6423
|
}
|
|
6335
|
-
const cert =
|
|
6336
|
-
const key =
|
|
6337
|
-
const ca =
|
|
6424
|
+
const cert = fs10.readFileSync(certs.certPath);
|
|
6425
|
+
const key = fs10.readFileSync(certs.keyPath);
|
|
6426
|
+
const ca = fs10.readFileSync(certs.caPath);
|
|
6338
6427
|
tlsOptions = {
|
|
6339
6428
|
cert,
|
|
6340
6429
|
key,
|
|
@@ -6359,10 +6448,10 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6359
6448
|
}
|
|
6360
6449
|
store.ensureDir();
|
|
6361
6450
|
const logPath = path9.join(stateDir, "proxy.log");
|
|
6362
|
-
const logFd =
|
|
6451
|
+
const logFd = fs10.openSync(logPath, "a");
|
|
6363
6452
|
try {
|
|
6364
6453
|
try {
|
|
6365
|
-
|
|
6454
|
+
fs10.chmodSync(logPath, FILE_MODE);
|
|
6366
6455
|
} catch {
|
|
6367
6456
|
}
|
|
6368
6457
|
fixOwnership(logPath);
|
|
@@ -6394,13 +6483,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6394
6483
|
});
|
|
6395
6484
|
child.unref();
|
|
6396
6485
|
} finally {
|
|
6397
|
-
|
|
6486
|
+
fs10.closeSync(logFd);
|
|
6398
6487
|
}
|
|
6399
6488
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
6400
6489
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
6401
6490
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
6402
6491
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
6403
|
-
if (
|
|
6492
|
+
if (fs10.existsSync(logPath)) {
|
|
6404
6493
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
6405
6494
|
}
|
|
6406
6495
|
process.exit(1);
|
|
@@ -6553,7 +6642,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes)
|
|
|
6553
6642
|
};
|
|
6554
6643
|
if (tls2) {
|
|
6555
6644
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
6556
|
-
if (
|
|
6645
|
+
if (fs10.existsSync(caPath)) {
|
|
6557
6646
|
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
6558
6647
|
}
|
|
6559
6648
|
}
|
|
@@ -6743,7 +6832,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName,
|
|
|
6743
6832
|
};
|
|
6744
6833
|
if (tls2) {
|
|
6745
6834
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
6746
|
-
if (
|
|
6835
|
+
if (fs10.existsSync(caPath)) {
|
|
6747
6836
|
entry.NODE_EXTRA_CA_CERTS = caPath;
|
|
6748
6837
|
}
|
|
6749
6838
|
}
|