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 +8 -3
- package/dist/{chunk-FM7IA4AF.js → chunk-EZJWUTUA.js} +47 -8
- package/dist/cli.js +97 -31
- package/dist/index.js +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
|
|
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:
|
|
1006
|
-
tld:
|
|
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
|
|
1276
|
-
var
|
|
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(
|
|
1341
|
-
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1699
|
-
const
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1729
|
-
customCertPath:
|
|
1730
|
-
customKeyPath:
|
|
1731
|
-
lanMode:
|
|
1732
|
-
lanIp:
|
|
1733
|
-
lanIpExplicit:
|
|
1734
|
-
tld:
|
|
1735
|
-
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(
|
|
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
|
-
|
|
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.
|
|
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