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/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-T7CGCIRO.js";
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 fs9 from "fs";
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 fs from "fs";
54
+ import * as fs2 from "fs";
54
55
  import * as path from "path";
55
- import * as crypto from "crypto";
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
- fs.accessSync(filePath, fs.constants.R_OK);
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 = fs.readFileSync(caCertPath);
84
- return crypto.createHash("sha256").update(pem).digest("hex");
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 = fs.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
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
- fs.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), fp + "\n");
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
- fs.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
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 = fs.readFileSync(certPath, "utf-8");
147
- const cert = new crypto.X509Certificate(pem);
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 execFileSync("openssl", args, {
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
- fs.chmodSync(keyPath, 384);
230
- fs.chmodSync(certPath, 420);
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
- fs.writeFileSync(
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
- fs.writeFileSync(
345
+ fs2.writeFileSync(
258
346
  srlPath,
259
- crypto.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
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
- fs.unlinkSync(tmp);
371
+ fs2.unlinkSync(tmp);
284
372
  } catch {
285
373
  }
286
374
  }
287
- fs.chmodSync(serverKeyPath, 384);
288
- fs.chmodSync(serverCertPath, 420);
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
- if (fp && marker === fp) return true;
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
- return isCATrustedLinux(stateDir);
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 isCATrustedWindows(caCertPath);
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
- execFileSync(
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
- execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
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 = execFileSync("security", ["default-keychain"], {
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 = fs.readFileSync("/etc/os-release", "utf-8").toLowerCase();
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
- execFileSync("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
411
- if (fs.existsSync(path.dirname(config.certDir))) return distro;
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 = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
427
- const installed = fs.readFileSync(systemCertPath, "utf-8").trim();
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 (!fs.existsSync(hostDir)) {
443
- await fs.promises.mkdir(hostDir, { recursive: true, mode: 493 });
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 fs.promises.writeFile(
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 (!fs.existsSync(srlPath)) {
471
- await fs.promises.writeFile(
551
+ if (!fs2.existsSync(srlPath)) {
552
+ await fs2.promises.writeFile(
472
553
  srlPath,
473
- crypto.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
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 fs.promises.unlink(tmp);
578
+ await fs2.promises.unlink(tmp);
498
579
  } catch {
499
580
  }
500
581
  }
501
- await fs.promises.chmod(keyPath, 384);
502
- await fs.promises.chmod(certPath, 420);
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 = fs.readFileSync(certPath);
610
+ const hostCert = fs2.readFileSync(certPath);
530
611
  const ctx = tls.createSecureContext({
531
612
  cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
532
- key: fs.readFileSync(keyPath)
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
- fs.promises.readFile(generated.certPath),
547
- fs.promises.readFile(generated.keyPath)
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
- execFileSync(
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
- execFileSync(
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 (!fs.existsSync(config.certDir)) {
603
- fs.mkdirSync(config.certDir, { recursive: true });
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
- fs.copyFileSync(caCertPath, dest);
607
- execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
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
- execFileSync("certutil", ["-addstore", "-user", "Root", caCertPath], {
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
- if (!isCATrusted(stateDir)) {
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 = untrustCAWindows(caCertPath);
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
- execFileSync("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
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
- execFileSync(
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
- execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
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
- let deletedAny = false;
707
- for (const config of Object.values(LINUX_CA_TRUST_CONFIGS)) {
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 = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
712
- const installed = fs.readFileSync(dest, "utf-8").trim();
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
- fs.unlinkSync(dest);
715
- deletedAny = true;
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 (deletedAny) {
810
+ if (refreshNeeded) {
723
811
  try {
724
- const config = getLinuxCATrustConfig();
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 (isCATrusted(stateDir)) {
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 untrustCAWindows(caCertPath) {
826
+ function untrustCAWSL(stateDir, caCertPath) {
827
+ const linuxResult = untrustCALinux(stateDir);
828
+ let windowsResult;
739
829
  try {
740
- const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
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
- return { removed: false, error: err instanceof Error ? err.message : String(err) };
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 defaultRunner(args) {
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 ?? defaultRunner;
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 = defaultRunner) {
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 ?? defaultRunner;
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 ?? defaultRunner);
1074
+ register("serve", localPort, httpsPort, options?.runner ?? defaultRunner2);
993
1075
  }
994
1076
  function registerFunnel(localPort, httpsPort, options) {
995
- register("funnel", localPort, httpsPort, options?.runner ?? defaultRunner);
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 defaultRunner2(args) {
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 = defaultRunner2) {
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 execFileSync2 } from "child_process";
1183
- import * as fs2 from "fs";
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 = fs2.readFileSync(pkgPath, "utf-8");
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 = execFileSync2("git", ["rev-parse", "--show-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 = fs2.statSync(gitPath);
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 = execFileSync2("git", ["worktree", "list", "--porcelain"], {
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
- execFileSync2("git", ["rev-parse", "--git-dir"], {
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
- execFileSync2("git", ["rev-parse", "--git-common-dir"], {
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 = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
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 = fs2.statSync(gitPath);
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 = fs2.readFileSync(gitPath, "utf-8").trim();
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 = fs2.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
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 fs3 from "fs";
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 os from "os";
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(os.tmpdir(), "portless") : "/tmp/portless";
1372
- var USER_STATE_DIR = path3.join(os.homedir(), ".portless");
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 = fs3.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
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 fs3.existsSync(path3.join(dir, TLS_MARKER_FILE));
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
- fs3.writeFileSync(markerPath, "1", { mode: 420 });
1608
+ fs4.writeFileSync(markerPath, "1", { mode: 420 });
1527
1609
  } else {
1528
1610
  try {
1529
- fs3.unlinkSync(markerPath);
1611
+ fs4.unlinkSync(markerPath);
1530
1612
  } catch {
1531
1613
  }
1532
1614
  }
1533
1615
  }
1534
1616
  function readCustomCertMarker(dir) {
1535
1617
  try {
1536
- return fs3.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
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
- fs3.writeFileSync(markerPath, "1", { mode: 420 });
1626
+ fs4.writeFileSync(markerPath, "1", { mode: 420 });
1545
1627
  } else {
1546
1628
  try {
1547
- fs3.unlinkSync(markerPath);
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 = fs3.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
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
- fs3.unlinkSync(markerPath);
1647
+ fs4.unlinkSync(markerPath);
1566
1648
  } catch {
1567
1649
  }
1568
1650
  } else {
1569
- fs3.writeFileSync(markerPath, ip, { mode: 420 });
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 = fs3.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
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 = fs3.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
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
- fs3.unlinkSync(tldsPath);
1718
+ fs4.unlinkSync(tldsPath);
1637
1719
  } catch {
1638
1720
  }
1639
1721
  try {
1640
- fs3.unlinkSync(tldPath);
1722
+ fs4.unlinkSync(tldPath);
1641
1723
  } catch {
1642
1724
  }
1643
1725
  } else {
1644
- fs3.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
1645
- fs3.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
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 (fs3.existsSync(bin)) {
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 fs4 from "fs";
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 (fs4.existsSync(resolved)) dirs.add(resolved);
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 removePortlessStateFiles(dir) {
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
- fs4.unlinkSync(path4.join(dir, f));
2223
+ fs5.unlinkSync(path4.join(dir, f));
2130
2224
  } catch {
2131
2225
  }
2132
2226
  }
2133
2227
  try {
2134
- fs4.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
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 fs5 from "fs";
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 = fs5.readFileSync(configPath, "utf-8");
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 = fs5.readFileSync(pkgPath, "utf-8");
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 = fs5.readFileSync(pkgPath, "utf-8");
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 = fs5.readFileSync(pkgPath, "utf-8");
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 = fs5.readFileSync(pkgPath, "utf-8");
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
- fs5.accessSync(path5.join(dir, file), fs5.constants.F_OK);
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 fs6 from "fs";
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
- fs6.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs6.constants.R_OK);
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
- fs6.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs6.constants.R_OK);
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 = fs6.readFileSync(pkgPath, "utf-8");
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 = fs6.readFileSync(wsPath, "utf-8");
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 = fs6.readFileSync(pkgPath, "utf-8");
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 = fs6.statSync(base);
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 = fs6.readdirSync(base, { withFileTypes: true });
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 fs7 from "fs";
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
- fs7.mkdirSync(baseDir, { recursive: true, mode: 493 });
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 = fs7.readFileSync(target, "utf-8");
2927
+ const existing = fs8.readFileSync(target, "utf-8");
2834
2928
  if (existing === source) return;
2835
2929
  } catch {
2836
2930
  }
2837
- fs7.writeFileSync(target, source, { mode: 420 });
2931
+ fs8.writeFileSync(target, source, { mode: 420 });
2838
2932
  }
2839
2933
  function writeManifest(entries, baseDir = USER_STATE_DIR) {
2840
- fs7.mkdirSync(baseDir, { recursive: true, mode: 493 });
2841
- fs7.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
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
- fs7.unlinkSync(manifestPath(baseDir));
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
- fs7.accessSync(path7.join(wsRoot, "turbo.json"), fs7.constants.R_OK);
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 fs8 from "fs";
2866
- import * as os2 from "os";
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 defaultRunner3(command, args, options) {
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 === "~" ? os2.homedir() : value.startsWith("~/") || value.startsWith("~\\") ? path8.join(os2.homedir(), value.slice(2)) : 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 = process.env.USERPROFILE || os2.homedir();
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 = process.env.HOME && process.env.HOME !== "/var/root" && process.env.HOME !== "/root" ? process.env.HOME : readPasswdHome(sudoUser) || (platform === "darwin" ? path8.posix.join("/Users", sudoUser) : path8.posix.join("/home", sudoUser));
3170
+ const home = resolveUserHome({ platform });
3090
3171
  return { home, uid: sudoUid, gid: sudoGid, username: sudoUser };
3091
3172
  }
3092
- const userInfo2 = os2.userInfo();
3173
+ const userInfo2 = os3.userInfo();
3093
3174
  return {
3094
- home: os2.homedir(),
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 (!fs8.existsSync(spec.plistPath)) return null;
3373
- const plist = fs8.readFileSync(spec.plistPath, "utf-8");
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 (!fs8.existsSync(spec.unitPath)) return null;
3384
- const unit = fs8.readFileSync(spec.unitPath, "utf-8");
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 (!fs8.existsSync(spec.scriptPath)) return null;
3403
- const script = fs8.readFileSync(spec.scriptPath, "utf-8");
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 ?? os2.homedir();
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 = os2.homedir();
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
- fs8.mkdirSync(stateDir, { recursive: true });
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
- fs8.writeFileSync(spec.plistPath, spec.plist);
3585
- fs8.chmodSync(spec.plistPath, 420);
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
- fs8.writeFileSync(spec.unitPath, spec.unit);
3594
- fs8.chmodSync(spec.unitPath, 420);
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
- fs8.mkdirSync(spec.scriptDir, { recursive: true });
3602
- fs8.writeFileSync(spec.scriptPath, spec.script);
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
- fs8.rmSync(spec.plistPath, { force: true });
3696
+ fs9.rmSync(spec.plistPath, { force: true });
3616
3697
  } else if (spec.platform === "linux") {
3617
3698
  runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
3618
- fs8.rmSync(spec.unitPath, { force: true });
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
- fs8.rmSync(spec.scriptDir, { recursive: true, force: true });
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 = defaultRunner3) {
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 = fs8.existsSync(spec.plistPath);
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
- fs8.rmSync(spec.plistPath, { force: true });
3716
+ fs9.rmSync(spec.plistPath, { force: true });
3636
3717
  } else if (spec.platform === "linux") {
3637
- installed = fs8.existsSync(spec.unitPath);
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
- fs8.rmSync(spec.unitPath, { force: true });
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
- fs8.rmSync(spec.scriptDir, { recursive: true, force: true });
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 = fs8.existsSync(spec.plistPath);
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 || fs8.existsSync(spec.unitPath);
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 || defaultRunner3;
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 (fs9.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
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 (!fs9.existsSync(routesPath)) {
4067
- fs9.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
4147
+ if (!fs10.existsSync(routesPath)) {
4148
+ fs10.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
4068
4149
  }
4069
4150
  try {
4070
- fs9.chmodSync(routesPath, FILE_MODE);
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 = fs9.watch(routesPath, () => {
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
- fs9.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
4182
- fs9.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
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
- fs9.unlinkSync(store.pidPath);
4312
+ fs10.unlinkSync(store.pidPath);
4232
4313
  } catch {
4233
4314
  }
4234
4315
  try {
4235
- fs9.unlinkSync(store.portFilePath);
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 (!fs9.existsSync(pidPath)) {
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
- fs9.unlinkSync(store.portFilePath);
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(fs9.readFileSync(pidPath, "utf-8"), 10);
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
- fs9.unlinkSync(pidPath);
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
- fs9.unlinkSync(pidPath);
4412
+ fs10.unlinkSync(pidPath);
4332
4413
  try {
4333
- fs9.unlinkSync(store.portFilePath);
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
- fs9.unlinkSync(pidPath);
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
- fs9.unlinkSync(pidPath);
4438
+ fs10.unlinkSync(pidPath);
4358
4439
  try {
4359
- fs9.unlinkSync(store.portFilePath);
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 (fs9.existsSync(logPath)) {
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 (fs9.existsSync(caPath)) {
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.2");
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 (!fs9.existsSync(dir)) {
5166
- fs9.mkdirSync(dir, { recursive: true });
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
- for (const stateDir of stateDirs) {
5271
- const caPath = path9.join(stateDir, "ca.pem");
5272
- if (!fs9.existsSync(caPath)) continue;
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 if (untrustResult.error) {
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
- fs9.accessSync(targetPath, fs9.constants.W_OK);
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 (fs9.existsSync(current)) return current;
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 = fs9.existsSync(store.portFilePath);
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 = fs9.existsSync(state.dir);
5797
+ const stateExists = fs10.existsSync(state.dir);
5709
5798
  console.log(colors_default.blue.bold("\nportless doctor\n"));
5710
- console.log(`Version: ${"0.15.2"}`);
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 = fs9.statSync(state.dir);
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 = fs9.statSync(ancestor);
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 (fs9.existsSync(store.pidPath)) {
5862
+ if (fs10.existsSync(store.pidPath)) {
5774
5863
  try {
5775
- const rawPid = fs9.readFileSync(store.pidPath, "utf-8").trim();
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 (fs9.existsSync(caPath)) {
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 (fs9.existsSync(logPath2)) {
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 = fs9.readFileSync(customCertPath);
6290
- const key = fs9.readFileSync(customKeyPath);
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 = fs9.readFileSync(certs.certPath);
6336
- const key = fs9.readFileSync(certs.keyPath);
6337
- const ca = fs9.readFileSync(certs.caPath);
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 = fs9.openSync(logPath, "a");
6451
+ const logFd = fs10.openSync(logPath, "a");
6363
6452
  try {
6364
6453
  try {
6365
- fs9.chmodSync(logPath, FILE_MODE);
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
- fs9.closeSync(logFd);
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 (fs9.existsSync(logPath)) {
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 (fs9.existsSync(caPath)) {
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 (fs9.existsSync(caPath)) {
6835
+ if (fs10.existsSync(caPath)) {
6747
6836
  entry.NODE_EXTRA_CA_CERTS = caPath;
6748
6837
  }
6749
6838
  }