portless 0.10.1 → 0.10.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 CHANGED
@@ -32,7 +32,11 @@ portless myapp next dev
32
32
 
33
33
  HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use `--no-tls` for plain HTTP.
34
34
 
35
- The proxy auto-starts when you run an app. A random port (4000--4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag.
35
+ The proxy auto-starts when you run an app. A random port (4000-4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag.
36
+
37
+ When auto-starting, portless reuses the configuration (port, TLS, TLD) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars (`PORTLESS_PORT`, `PORTLESS_HTTPS`, etc.) always take priority.
38
+
39
+ In non-interactive environments (no TTY, or `CI=1`), portless exits with a descriptive error instead of prompting, so task runners like turborepo and CI scripts fail early with a clear message.
36
40
 
37
41
  ## Use in package.json
38
42
 
@@ -139,7 +143,7 @@ portless proxy start --lan --ip 192.168.1.42
139
143
 
140
144
  `--lan` switches the proxy to mDNS discovery: services are advertised as `<name>.local` and reachable from any device on the same network. Portless auto-detects your LAN IP and follows Wi-Fi/IP changes automatically, but you can pin another address with `--ip <address>` or by exporting `PORTLESS_LAN_IP`. Set `PORTLESS_LAN=1` in your shell (0/1 boolean) to make LAN mode the default whenever the proxy starts.
141
145
 
142
- Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. Other proxy settings still follow the current flags and env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.
146
+ Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. All proxy settings (port, TLS, TLD, LAN) are persisted and reused on auto-start unless overridden by explicit flags or env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.
143
147
 
144
148
  LAN mode depends on the system mDNS tools that portless already spawns: macOS ships with `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s equivalent). If the command is missing or your network isn’t reachable, `portless proxy start --lan` prints the relevant error and exits.
145
149
 
@@ -218,6 +222,7 @@ PORTLESS_STATE_DIR=<path> Override the state directory
218
222
  PORT Ephemeral port the child should listen on
219
223
  HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
220
224
  PORTLESS_URL Public URL (e.g. https://myapp.localhost)
225
+ NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
221
226
  ```
222
227
 
223
228
  > **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
@@ -275,7 +280,7 @@ devServer: {
275
280
  }
276
281
  ```
277
282
 
278
- If your tooling doesn't trust the portless CA, point Node.js at it: `NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` (or `~/.portless/ca.pem` when the proxy runs on a non-privileged port like 1355). Alternatively, use `--no-tls` for plain HTTP.
283
+ Portless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` (or `~/.portless/ca.pem` when the proxy runs on a non-privileged port like 1355). Alternatively, use `--no-tls` for plain HTTP.
279
284
 
280
285
  Portless detects this misconfiguration and responds with `508 Loop Detected` along with a message pointing to this fix.
281
286
 
@@ -392,8 +392,10 @@ function createProxyServer(options) {
392
392
  proxyRes.on("error", () => {
393
393
  if (!res.headersSent) {
394
394
  res.writeHead(502, { "Content-Type": "text/plain" });
395
+ res.end();
396
+ } else {
397
+ res.destroy();
395
398
  }
396
- res.end();
397
399
  });
398
400
  proxyRes.pipe(res);
399
401
  }
@@ -516,9 +518,19 @@ function createProxyServer(options) {
516
518
  cert: tls.ca ? Buffer.concat([tls.cert, tls.ca]) : tls.cert,
517
519
  key: tls.key,
518
520
  allowHTTP1: true,
521
+ // Tolerate high rates of RST_STREAM from browsers during HMR and
522
+ // page navigations. Without this, Node sends GOAWAY INTERNAL_ERROR
523
+ // after ~1000 cumulative stream resets and kills the session,
524
+ // surfacing as ERR_HTTP2_PROTOCOL_ERROR in Chrome. Available in
525
+ // Node 22.11+; silently ignored on older versions.
526
+ ...{ streamResetBurst: 1e4, streamResetRate: 100 },
519
527
  ...tls.SNICallback ? { SNICallback: tls.SNICallback } : {}
520
528
  });
529
+ h2Server.on("sessionError", () => {
530
+ });
521
531
  h2Server.on("request", (req, res) => {
532
+ req.stream?.on("error", () => {
533
+ });
522
534
  handleRequest(req, res);
523
535
  });
524
536
  h2Server.on("upgrade", (req, socket, head) => {
@@ -901,6 +913,24 @@ function isLanEnvEnabled() {
901
913
  const val = process.env.PORTLESS_LAN;
902
914
  return val === "1" || val === "true";
903
915
  }
916
+ function readPersistedProxyState() {
917
+ const dirs = [];
918
+ if (process.env.PORTLESS_STATE_DIR) {
919
+ dirs.push(process.env.PORTLESS_STATE_DIR);
920
+ } else {
921
+ dirs.push(USER_STATE_DIR, SYSTEM_STATE_DIR);
922
+ }
923
+ for (const dir of dirs) {
924
+ const port = readPortFromDir(dir);
925
+ if (port !== null) {
926
+ const tls = readTlsMarker(dir);
927
+ const tld = readTldFromDir(dir);
928
+ const lanIp = readLanMarker(dir);
929
+ return { port, tls, tld, lanMode: lanIp !== null || tld === "local" };
930
+ }
931
+ }
932
+ return null;
933
+ }
904
934
  function buildProxyStartConfig(options) {
905
935
  const effectiveTld = options.lanMode ? "local" : options.tld;
906
936
  const args = [];
@@ -934,6 +964,9 @@ function buildProxyStartConfig(options) {
934
964
  if (options.useWildcard) {
935
965
  args.push("--wildcard");
936
966
  }
967
+ if (options.skipTrust) {
968
+ args.push("--skip-trust");
969
+ }
937
970
  return { effectiveTld, args };
938
971
  }
939
972
  async function discoverState() {
@@ -1002,8 +1035,8 @@ async function discoverState() {
1002
1035
  return {
1003
1036
  dir,
1004
1037
  port: configuredPort,
1005
- tls: false,
1006
- tld: getDefaultTld(),
1038
+ tls: readTlsMarker(dir),
1039
+ tld: readTldFromDir(dir),
1007
1040
  lanMode: readLanMarker(dir) !== null,
1008
1041
  lanIp: null
1009
1042
  };
@@ -1272,8 +1305,9 @@ function prompt(question) {
1272
1305
  import * as fs4 from "fs";
1273
1306
  import * as path3 from "path";
1274
1307
  var STALE_LOCK_THRESHOLD_MS = 1e4;
1275
- var LOCK_MAX_RETRIES = 20;
1276
- var LOCK_RETRY_DELAY_MS = 50;
1308
+ var LOCK_TIMEOUT_MS = 5e3;
1309
+ var LOCK_RETRY_BASE_MS = 10;
1310
+ var LOCK_RETRY_CAP_MS = 500;
1277
1311
  var FILE_MODE = 420;
1278
1312
  var DIR_MODE = 493;
1279
1313
  var SYSTEM_DIR_MODE = 1023;
@@ -1337,8 +1371,10 @@ var RouteStore = class _RouteStore {
1337
1371
  syncSleep(ms) {
1338
1372
  Atomics.wait(_RouteStore.sleepBuffer, 0, 0, ms);
1339
1373
  }
1340
- acquireLock(maxRetries = LOCK_MAX_RETRIES, retryDelayMs = LOCK_RETRY_DELAY_MS) {
1341
- for (let i = 0; i < maxRetries; i++) {
1374
+ acquireLock() {
1375
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
1376
+ let delay = LOCK_RETRY_BASE_MS;
1377
+ while (Date.now() < deadline) {
1342
1378
  try {
1343
1379
  fs4.mkdirSync(this.lockPath);
1344
1380
  return true;
@@ -1353,7 +1389,9 @@ var RouteStore = class _RouteStore {
1353
1389
  } catch {
1354
1390
  continue;
1355
1391
  }
1356
- this.syncSleep(retryDelayMs);
1392
+ const jitter = Math.floor(Math.random() * delay);
1393
+ this.syncSleep(delay + jitter);
1394
+ delay = Math.min(delay * 2, LOCK_RETRY_CAP_MS);
1357
1395
  } else {
1358
1396
  return false;
1359
1397
  }
@@ -1507,6 +1545,7 @@ export {
1507
1545
  isHttpsEnvDisabled,
1508
1546
  isWildcardEnvEnabled,
1509
1547
  isLanEnvEnabled,
1548
+ readPersistedProxyState,
1510
1549
  buildProxyStartConfig,
1511
1550
  discoverState,
1512
1551
  findFreePort,
package/dist/cli.js CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  parseHostname,
36
36
  prompt,
37
37
  readLanMarker,
38
+ readPersistedProxyState,
38
39
  readTldFromDir,
39
40
  readTlsMarker,
40
41
  resolveStateDir,
@@ -46,7 +47,7 @@ import {
46
47
  writeLanMarker,
47
48
  writeTldFile,
48
49
  writeTlsMarker
49
- } from "./chunk-FM7IA4AF.js";
50
+ } from "./chunk-EZJWUTUA.js";
50
51
 
51
52
  // src/colors.ts
52
53
  function supportsColor() {
@@ -90,6 +91,9 @@ var SERVER_VALIDITY_DAYS = 365;
90
91
  var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
91
92
  var CA_COMMON_NAME = "portless Local CA";
92
93
  var OPENSSL_TIMEOUT_MS = 15e3;
94
+ var MACOS_SECURITY_TIMEOUT_MS = 15e3;
95
+ var MACOS_SECURITY_AUTH_TIMEOUT_MS = 12e4;
96
+ var MACOS_SECURITY_ROOT_TIMEOUT_MS = 6e4;
93
97
  var CA_KEY_FILE = "ca-key.pem";
94
98
  var CA_CERT_FILE = "ca.pem";
95
99
  var SERVER_KEY_FILE = "server-key.pem";
@@ -338,13 +342,13 @@ function isCATrustedMacOS(caCertPath) {
338
342
  ["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
339
343
  {
340
344
  stdio: "pipe",
341
- timeout: 5e3
345
+ timeout: MACOS_SECURITY_TIMEOUT_MS
342
346
  }
343
347
  );
344
348
  } else {
345
349
  execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
346
350
  stdio: "pipe",
347
- timeout: 5e3
351
+ timeout: MACOS_SECURITY_TIMEOUT_MS
348
352
  });
349
353
  }
350
354
  return true;
@@ -356,7 +360,7 @@ function loginKeychainPath() {
356
360
  try {
357
361
  const result = execFileSync("security", ["default-keychain"], {
358
362
  encoding: "utf-8",
359
- timeout: 5e3
363
+ timeout: MACOS_SECURITY_TIMEOUT_MS
360
364
  }).trim();
361
365
  const match = result.match(/"(.+)"/);
362
366
  if (match) return match[1];
@@ -572,14 +576,14 @@ function trustCA(stateDir) {
572
576
  "/Library/Keychains/System.keychain",
573
577
  caCertPath
574
578
  ],
575
- { stdio: "pipe", timeout: 3e4 }
579
+ { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS }
576
580
  );
577
581
  } else {
578
582
  const keychain = loginKeychainPath();
579
583
  execFileSync(
580
584
  "security",
581
585
  ["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
582
- { stdio: "pipe", timeout: 3e4 }
586
+ { stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
583
587
  );
584
588
  }
585
589
  return { trusted: true };
@@ -602,6 +606,10 @@ function trustCA(stateDir) {
602
606
  return { trusted: false, error: `Unsupported platform: ${process.platform}` };
603
607
  } catch (err) {
604
608
  const message = err instanceof Error ? err.message : String(err);
609
+ if (message.includes("ETIMEDOUT")) {
610
+ const hint = process.platform === "darwin" ? "The macOS security command timed out. This can happen when the Keychain Services daemon is unresponsive or a system authorization dialog was not dismissed in time. Try restarting Keychain Access (or run: sudo killall securityd) and then: portless trust" : "The trust command timed out. Try: portless trust";
611
+ return { trusted: false, error: hint };
612
+ }
605
613
  if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
606
614
  return {
607
615
  trusted: false,
@@ -639,7 +647,7 @@ function untrustCAMacOS(caCertPath) {
639
647
  const errors = [];
640
648
  const tryExec = (args) => {
641
649
  try {
642
- execFileSync("security", args, { stdio: "pipe", timeout: 3e4 });
650
+ execFileSync("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
643
651
  return true;
644
652
  } catch (err) {
645
653
  const message = err instanceof Error ? err.message : String(err);
@@ -663,12 +671,12 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
663
671
  execFileSync(
664
672
  "sudo",
665
673
  ["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
666
- { stdio: "pipe", timeout: 5e3 }
674
+ { stdio: "pipe", timeout: MACOS_SECURITY_TIMEOUT_MS }
667
675
  );
668
676
  } else {
669
677
  execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
670
678
  stdio: "pipe",
671
- timeout: 5e3
679
+ timeout: MACOS_SECURITY_TIMEOUT_MS
672
680
  });
673
681
  }
674
682
  return true;
@@ -1522,6 +1530,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
1522
1530
  }
1523
1531
  writeTlsMarker(store.dir, false);
1524
1532
  writeTldFile(store.dir, DEFAULT_TLD);
1533
+ writeLanMarker(store.dir, null);
1525
1534
  if (autoSyncHosts) cleanHostsFile();
1526
1535
  server.close(() => process.exit(0));
1527
1536
  setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
@@ -1557,6 +1566,9 @@ async function stopProxy(store, proxyPort, _tls) {
1557
1566
  fs4.unlinkSync(store.portFilePath);
1558
1567
  } catch {
1559
1568
  }
1569
+ writeTlsMarker(store.dir, false);
1570
+ writeTldFile(store.dir, DEFAULT_TLD);
1571
+ writeLanMarker(store.dir, null);
1560
1572
  console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
1561
1573
  } catch (err) {
1562
1574
  if (isErrnoException(err) && err.code === "EPERM") {
@@ -1593,6 +1605,9 @@ async function stopProxy(store, proxyPort, _tls) {
1593
1605
  if (isNaN(pid)) {
1594
1606
  console.error(colors_default.red("Corrupted PID file. Removing it."));
1595
1607
  fs4.unlinkSync(pidPath);
1608
+ writeTlsMarker(store.dir, false);
1609
+ writeTldFile(store.dir, DEFAULT_TLD);
1610
+ writeLanMarker(store.dir, null);
1596
1611
  return;
1597
1612
  }
1598
1613
  try {
@@ -1608,6 +1623,9 @@ async function stopProxy(store, proxyPort, _tls) {
1608
1623
  fs4.unlinkSync(store.portFilePath);
1609
1624
  } catch {
1610
1625
  }
1626
+ writeTlsMarker(store.dir, false);
1627
+ writeTldFile(store.dir, DEFAULT_TLD);
1628
+ writeLanMarker(store.dir, null);
1611
1629
  return;
1612
1630
  }
1613
1631
  if (!await isProxyRunning(proxyPort)) {
@@ -1618,6 +1636,9 @@ async function stopProxy(store, proxyPort, _tls) {
1618
1636
  );
1619
1637
  console.log(colors_default.yellow("Removing stale PID file."));
1620
1638
  fs4.unlinkSync(pidPath);
1639
+ writeTlsMarker(store.dir, false);
1640
+ writeTldFile(store.dir, DEFAULT_TLD);
1641
+ writeLanMarker(store.dir, null);
1621
1642
  return;
1622
1643
  }
1623
1644
  process.kill(pid, "SIGTERM");
@@ -1626,6 +1647,9 @@ async function stopProxy(store, proxyPort, _tls) {
1626
1647
  fs4.unlinkSync(store.portFilePath);
1627
1648
  } catch {
1628
1649
  }
1650
+ writeTlsMarker(store.dir, false);
1651
+ writeTldFile(store.dir, DEFAULT_TLD);
1652
+ writeLanMarker(store.dir, null);
1629
1653
  console.log(colors_default.green("Proxy stopped."));
1630
1654
  } catch (err) {
1631
1655
  if (isErrnoException(err) && err.code === "EPERM") {
@@ -1695,11 +1719,30 @@ portless
1695
1719
  const proxyResponsive = await isProxyRunning(proxyPort, tls2);
1696
1720
  const proxyListeningFromStateDir = !!process.env.PORTLESS_STATE_DIR && await isPortListening(proxyPort);
1697
1721
  if (!proxyResponsive && !proxyListeningFromStateDir) {
1698
- const defaultPort = getDefaultPort(desiredConfig.useHttps);
1699
- const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
1700
- const manualStartCommand = formatProxyStartCommand(defaultPort, desiredConfig);
1701
- const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, desiredConfig);
1702
- if (needsSudo && !process.stdin.isTTY) {
1722
+ const persisted = readPersistedProxyState();
1723
+ const startConfig = { ...desiredConfig };
1724
+ let startPort;
1725
+ if (persisted) {
1726
+ if (!explicit.useHttps && persisted.tls !== desiredConfig.useHttps) {
1727
+ startConfig.useHttps = persisted.tls;
1728
+ }
1729
+ if (!explicit.tld && persisted.tld !== desiredConfig.tld) {
1730
+ startConfig.tld = persisted.tld;
1731
+ }
1732
+ if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
1733
+ startConfig.lanMode = persisted.lanMode;
1734
+ }
1735
+ const envPort = getDefaultPort(startConfig.useHttps);
1736
+ if (persisted.port !== envPort) {
1737
+ startPort = persisted.port;
1738
+ }
1739
+ }
1740
+ const effectivePort = startPort ?? getDefaultPort(startConfig.useHttps);
1741
+ const needsSudo = !isWindows && effectivePort < PRIVILEGED_PORT_THRESHOLD;
1742
+ const manualStartCommand = formatProxyStartCommand(effectivePort, startConfig);
1743
+ const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, startConfig);
1744
+ const isInteractive = !!process.stdin.isTTY && !process.env.CI;
1745
+ if (needsSudo && !isInteractive) {
1703
1746
  console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
1704
1747
  console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
1705
1748
  console.error(colors_default.cyan(` ${manualStartCommand}`));
@@ -1711,7 +1754,7 @@ portless
1711
1754
  console.error(colors_default.cyan(` ${fallbackStartCommand}`));
1712
1755
  process.exit(1);
1713
1756
  }
1714
- if (needsSudo && process.stdin.isTTY) {
1757
+ if (needsSudo) {
1715
1758
  const answer = await prompt(colors_default.yellow("Proxy not running. Start it? [Y/n/skip] "));
1716
1759
  if (answer === "n" || answer === "no") {
1717
1760
  console.log(colors_default.gray("Cancelled."));
@@ -1723,16 +1766,26 @@ portless
1723
1766
  return;
1724
1767
  }
1725
1768
  }
1726
- console.log(colors_default.yellow("Starting proxy..."));
1769
+ if (persisted && startPort !== void 0) {
1770
+ console.log(
1771
+ colors_default.yellow(
1772
+ `Starting proxy with previous configuration (port ${startPort}, ${startConfig.useHttps ? "HTTPS" : "HTTP"})...`
1773
+ )
1774
+ );
1775
+ } else {
1776
+ console.log(colors_default.yellow("Starting proxy..."));
1777
+ }
1727
1778
  const proxyStartConfig = buildProxyStartConfig({
1728
- useHttps: desiredConfig.useHttps,
1729
- customCertPath: desiredConfig.customCertPath,
1730
- customKeyPath: desiredConfig.customKeyPath,
1731
- lanMode: desiredConfig.lanMode,
1732
- lanIp: desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null,
1733
- lanIpExplicit: desiredConfig.lanIpExplicit,
1734
- tld: desiredConfig.tld,
1735
- useWildcard: desiredConfig.useWildcard
1779
+ useHttps: startConfig.useHttps,
1780
+ customCertPath: startConfig.customCertPath,
1781
+ customKeyPath: startConfig.customKeyPath,
1782
+ lanMode: startConfig.lanMode,
1783
+ lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
1784
+ lanIpExplicit: startConfig.lanIpExplicit,
1785
+ tld: startConfig.tld,
1786
+ useWildcard: startConfig.useWildcard,
1787
+ includePort: startPort !== void 0,
1788
+ proxyPort: startPort
1736
1789
  });
1737
1790
  const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
1738
1791
  const result = spawnSync2(process.execPath, startArgs, {
@@ -1752,7 +1805,7 @@ portless
1752
1805
  }
1753
1806
  if (!discovered) {
1754
1807
  console.error(colors_default.red("Failed to start proxy."));
1755
- const fallbackDir = resolveStateDir(getDefaultPort(desiredConfig.useHttps));
1808
+ const fallbackDir = resolveStateDir(effectivePort);
1756
1809
  const logPath = path4.join(fallbackDir, "proxy.log");
1757
1810
  console.error(colors_default.blue("Try starting it manually:"));
1758
1811
  console.error(colors_default.cyan(` ${manualStartCommand}`));
@@ -1837,9 +1890,17 @@ portless
1837
1890
  process.env.PORTLESS_LAN = "1";
1838
1891
  }
1839
1892
  injectFrameworkFlags(commandArgs, port);
1893
+ const caEnv = {};
1894
+ if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
1895
+ const caPath = path4.join(stateDir, "ca.pem");
1896
+ if (fs4.existsSync(caPath)) {
1897
+ caEnv.NODE_EXTRA_CA_CERTS = caPath;
1898
+ }
1899
+ }
1900
+ const caFragment = caEnv.NODE_EXTRA_CA_CERTS ? ` NODE_EXTRA_CA_CERTS="${caEnv.NODE_EXTRA_CA_CERTS}"` : "";
1840
1901
  console.log(
1841
1902
  chalk.gray(
1842
- `Running: PORT=${port}${hostBind ? ` HOST=${hostBind}` : ""} PORTLESS_URL=${finalUrl} ${commandArgs.join(" ")}
1903
+ `Running: PORT=${port}${hostBind ? ` HOST=${hostBind}` : ""} PORTLESS_URL=${finalUrl}${caFragment} ${commandArgs.join(" ")}
1843
1904
  `
1844
1905
  )
1845
1906
  );
@@ -1853,7 +1914,8 @@ portless
1853
1914
  // Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
1854
1915
  // baked-in pinging, making this env var ineffective. Expo handles its
1855
1916
  // own LAN discovery natively.
1856
- ...lanMode ? { PORTLESS_LAN: "1" } : {}
1917
+ ...lanMode ? { PORTLESS_LAN: "1" } : {},
1918
+ ...caEnv
1857
1919
  },
1858
1920
  onCleanup: () => {
1859
1921
  try {
@@ -2055,7 +2117,8 @@ ${colors_default.bold("LAN mode:")}
2055
2117
  Expo keeps Metro's default LAN host behavior in this mode.
2056
2118
  Auto-detected LAN IPs follow network changes automatically.
2057
2119
  Stopped LAN proxies keep LAN mode for the next start via proxy.lan.
2058
- Other proxy settings still follow the current flags and env vars.
2120
+ All proxy settings are persisted and reused on auto-start unless
2121
+ overridden by explicit flags or env vars.
2059
2122
  Use PORTLESS_LAN=0 for one start to switch back to .localhost mode.
2060
2123
  If a proxy is already running with different explicit LAN/TLS/TLD settings,
2061
2124
  stop it first.
@@ -2098,6 +2161,7 @@ ${colors_default.bold("Child process environment:")}
2098
2161
  HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
2099
2162
  PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
2100
2163
  PORTLESS_LAN Set to 1 when proxy is in LAN mode
2164
+ NODE_EXTRA_CA_CERTS Path to the portless CA (set when HTTPS is active)
2101
2165
 
2102
2166
  ${colors_default.bold("Safari / DNS:")}
2103
2167
  .localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
@@ -2119,7 +2183,7 @@ ${colors_default.bold("Reserved names:")}
2119
2183
  process.exit(0);
2120
2184
  }
2121
2185
  function printVersion() {
2122
- console.log("0.10.1");
2186
+ console.log("0.10.3");
2123
2187
  process.exit(0);
2124
2188
  }
2125
2189
  async function handleTrust() {
@@ -2517,6 +2581,7 @@ ${colors_default.bold("LAN mode (--lan):")}
2517
2581
  process.exit(isProxyHelp || !args[1] ? 0 : 1);
2518
2582
  }
2519
2583
  const isForeground = args.includes("--foreground");
2584
+ const skipTrust = args.includes("--skip-trust");
2520
2585
  const hasHttpsFlag = args.includes("--https");
2521
2586
  const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
2522
2587
  const wantHttps = !hasNoTls;
@@ -2816,7 +2881,7 @@ ${colors_default.bold("LAN mode (--lan):")}
2816
2881
  if (certs.caGenerated) {
2817
2882
  console.log(colors_default.green("Generated local CA certificate."));
2818
2883
  }
2819
- if (!isCATrusted(stateDir)) {
2884
+ if (!skipTrust && !isCATrusted(stateDir)) {
2820
2885
  console.log(colors_default.yellow("Adding CA to system trust store..."));
2821
2886
  const trustResult = trustCA(stateDir);
2822
2887
  if (trustResult.trusted) {
@@ -2874,7 +2939,8 @@ ${colors_default.bold("LAN mode (--lan):")}
2874
2939
  useWildcard: desiredWildcard,
2875
2940
  foreground: true,
2876
2941
  includePort: true,
2877
- proxyPort
2942
+ proxyPort,
2943
+ skipTrust: true
2878
2944
  }).args
2879
2945
  ];
2880
2946
  const child = spawn2(process.execPath, daemonArgs, {
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  removeBlock,
22
22
  shouldAutoSyncHosts,
23
23
  syncHostsFile
24
- } from "./chunk-FM7IA4AF.js";
24
+ } from "./chunk-EZJWUTUA.js";
25
25
  export {
26
26
  DIR_MODE,
27
27
  FILE_MODE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portless",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Replace port numbers with stable, named .localhost URLs. For humans and agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",