portless 0.10.0 → 0.10.1
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 +15 -4
- package/dist/{chunk-WXEE5QH6.js → chunk-FM7IA4AF.js} +6 -0
- package/dist/cli.js +313 -52
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ portless myapp next dev
|
|
|
88
88
|
# -> https://myapp.test
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
The proxy auto-syncs `/etc/hosts` for
|
|
91
|
+
The proxy auto-syncs `/etc/hosts` for route hostnames (including `.test`), so those domains resolve on your machine.
|
|
92
92
|
|
|
93
93
|
Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).
|
|
94
94
|
|
|
@@ -166,6 +166,7 @@ portless alias <name> <port> --force # Overwrite an existing route
|
|
|
166
166
|
portless alias --remove <name> # Remove a static route
|
|
167
167
|
portless list # Show active routes
|
|
168
168
|
portless trust # Add local CA to system trust store
|
|
169
|
+
portless clean # Remove state, CA trust entry, and hosts block
|
|
169
170
|
portless hosts sync # Add routes to /etc/hosts (fixes Safari)
|
|
170
171
|
portless hosts clean # Remove portless entries from /etc/hosts
|
|
171
172
|
|
|
@@ -210,7 +211,7 @@ PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
|
|
|
210
211
|
PORTLESS_LAN=1 Enable LAN mode when set to 1 (auto-detects LAN IP)
|
|
211
212
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
|
|
212
213
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
213
|
-
PORTLESS_SYNC_HOSTS=
|
|
214
|
+
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of /etc/hosts (on by default)
|
|
214
215
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
215
216
|
|
|
216
217
|
# Injected into child processes
|
|
@@ -219,7 +220,17 @@ HOST Usually 127.0.0.1 (omitted for Expo in LAN mode
|
|
|
219
220
|
PORTLESS_URL Public URL (e.g. https://myapp.localhost)
|
|
220
221
|
```
|
|
221
222
|
|
|
222
|
-
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, 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.
|
|
223
|
+
> **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.
|
|
224
|
+
|
|
225
|
+
## Uninstall / reset
|
|
226
|
+
|
|
227
|
+
To remove portless data from your machine (proxy state under `~/.portless` and the system state directory, the local CA from the OS trust store when portless installed it, and the portless block in `/etc/hosts`):
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
portless clean
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
macOS/Linux may prompt for `sudo`. Custom certificate paths passed with `--cert` and `--key` are not deleted.
|
|
223
234
|
|
|
224
235
|
## Safari / DNS
|
|
225
236
|
|
|
@@ -232,7 +243,7 @@ portless hosts sync # Add current routes to /etc/hosts
|
|
|
232
243
|
portless hosts clean # Clean up later
|
|
233
244
|
```
|
|
234
245
|
|
|
235
|
-
Auto-syncs `/etc/hosts` for custom TLDs
|
|
246
|
+
Auto-syncs `/etc/hosts` for route hostnames by default (`.localhost`, custom TLDs, LAN `.local`). Set `PORTLESS_SYNC_HOSTS=0` to disable.
|
|
236
247
|
|
|
237
248
|
## Proxying Between Portless Apps
|
|
238
249
|
|
|
@@ -614,6 +614,9 @@ function buildBlock(hostnames) {
|
|
|
614
614
|
${entries}
|
|
615
615
|
${MARKER_END}`;
|
|
616
616
|
}
|
|
617
|
+
function shouldAutoSyncHosts(syncVal) {
|
|
618
|
+
return syncVal !== "0" && syncVal !== "false";
|
|
619
|
+
}
|
|
617
620
|
function syncHostsFile(hostnames) {
|
|
618
621
|
try {
|
|
619
622
|
const content = readHostsFile();
|
|
@@ -1475,6 +1478,7 @@ export {
|
|
|
1475
1478
|
extractManagedBlock,
|
|
1476
1479
|
removeBlock,
|
|
1477
1480
|
buildBlock,
|
|
1481
|
+
shouldAutoSyncHosts,
|
|
1478
1482
|
syncHostsFile,
|
|
1479
1483
|
cleanHostsFile,
|
|
1480
1484
|
getManagedHostnames,
|
|
@@ -1484,6 +1488,8 @@ export {
|
|
|
1484
1488
|
PRIVILEGED_PORT_THRESHOLD,
|
|
1485
1489
|
INTERNAL_LAN_IP_ENV,
|
|
1486
1490
|
INTERNAL_LAN_IP_FLAG,
|
|
1491
|
+
SYSTEM_STATE_DIR,
|
|
1492
|
+
USER_STATE_DIR,
|
|
1487
1493
|
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
1488
1494
|
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
1489
1495
|
getDefaultPort,
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
RISKY_TLDS,
|
|
10
10
|
RouteConflictError,
|
|
11
11
|
RouteStore,
|
|
12
|
+
SYSTEM_STATE_DIR,
|
|
13
|
+
USER_STATE_DIR,
|
|
12
14
|
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
13
15
|
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
14
16
|
buildProxyStartConfig,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
readTldFromDir,
|
|
37
39
|
readTlsMarker,
|
|
38
40
|
resolveStateDir,
|
|
41
|
+
shouldAutoSyncHosts,
|
|
39
42
|
spawnCommand,
|
|
40
43
|
syncHostsFile,
|
|
41
44
|
validateTld,
|
|
@@ -43,7 +46,7 @@ import {
|
|
|
43
46
|
writeLanMarker,
|
|
44
47
|
writeTldFile,
|
|
45
48
|
writeTlsMarker
|
|
46
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-FM7IA4AF.js";
|
|
47
50
|
|
|
48
51
|
// src/colors.ts
|
|
49
52
|
function supportsColor() {
|
|
@@ -71,8 +74,8 @@ var gray = wrap("90", "39");
|
|
|
71
74
|
var colors_default = { bold, red, green, yellow, blue, cyan, white, gray };
|
|
72
75
|
|
|
73
76
|
// src/cli.ts
|
|
74
|
-
import * as
|
|
75
|
-
import * as
|
|
77
|
+
import * as fs4 from "fs";
|
|
78
|
+
import * as path4 from "path";
|
|
76
79
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
77
80
|
|
|
78
81
|
// src/certs.ts
|
|
@@ -608,6 +611,129 @@ function trustCA(stateDir) {
|
|
|
608
611
|
return { trusted: false, error: message };
|
|
609
612
|
}
|
|
610
613
|
}
|
|
614
|
+
function untrustCA(stateDir) {
|
|
615
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
616
|
+
if (!fileExists(caCertPath)) {
|
|
617
|
+
return { removed: true };
|
|
618
|
+
}
|
|
619
|
+
if (!isCATrusted(stateDir)) {
|
|
620
|
+
return { removed: true };
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
if (process.platform === "darwin") {
|
|
624
|
+
return untrustCAMacOS(caCertPath);
|
|
625
|
+
}
|
|
626
|
+
if (process.platform === "linux") {
|
|
627
|
+
return untrustCALinux(stateDir);
|
|
628
|
+
}
|
|
629
|
+
if (process.platform === "win32") {
|
|
630
|
+
return untrustCAWindows(caCertPath);
|
|
631
|
+
}
|
|
632
|
+
return { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
633
|
+
} catch (err) {
|
|
634
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
635
|
+
return { removed: false, error: message };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function untrustCAMacOS(caCertPath) {
|
|
639
|
+
const errors = [];
|
|
640
|
+
const tryExec = (args) => {
|
|
641
|
+
try {
|
|
642
|
+
execFileSync("security", args, { stdio: "pipe", timeout: 3e4 });
|
|
643
|
+
return true;
|
|
644
|
+
} catch (err) {
|
|
645
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
646
|
+
errors.push(message);
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
if (tryExec(["remove-trusted-cert", caCertPath])) {
|
|
651
|
+
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Trust entry may still be present" } : { removed: true };
|
|
652
|
+
}
|
|
653
|
+
const login = loginKeychainPath();
|
|
654
|
+
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, login]);
|
|
655
|
+
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, "/Library/Keychains/System.keychain"]);
|
|
656
|
+
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Could not remove CA from keychain (try sudo)" } : { removed: true };
|
|
657
|
+
}
|
|
658
|
+
function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
659
|
+
try {
|
|
660
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
661
|
+
const sudoUser = process.env.SUDO_USER;
|
|
662
|
+
if (isRoot && sudoUser) {
|
|
663
|
+
execFileSync(
|
|
664
|
+
"sudo",
|
|
665
|
+
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
666
|
+
{ stdio: "pipe", timeout: 5e3 }
|
|
667
|
+
);
|
|
668
|
+
} else {
|
|
669
|
+
execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
670
|
+
stdio: "pipe",
|
|
671
|
+
timeout: 5e3
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
} catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function untrustCALinux(stateDir) {
|
|
680
|
+
const errors = [];
|
|
681
|
+
let deletedAny = false;
|
|
682
|
+
for (const config of Object.values(LINUX_CA_TRUST_CONFIGS)) {
|
|
683
|
+
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
684
|
+
try {
|
|
685
|
+
if (fileExists(dest)) {
|
|
686
|
+
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
687
|
+
const installed = fs.readFileSync(dest, "utf-8").trim();
|
|
688
|
+
if (ours === installed) {
|
|
689
|
+
fs.unlinkSync(dest);
|
|
690
|
+
deletedAny = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (deletedAny) {
|
|
698
|
+
try {
|
|
699
|
+
const config = getLinuxCATrustConfig();
|
|
700
|
+
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
701
|
+
} catch (err) {
|
|
702
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (isCATrusted(stateDir)) {
|
|
706
|
+
return {
|
|
707
|
+
removed: false,
|
|
708
|
+
error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return { removed: true };
|
|
712
|
+
}
|
|
713
|
+
function untrustCAWindows(caCertPath) {
|
|
714
|
+
try {
|
|
715
|
+
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
716
|
+
const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
717
|
+
encoding: "utf-8",
|
|
718
|
+
timeout: 1e4,
|
|
719
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
720
|
+
});
|
|
721
|
+
const normalized = storeListing.replace(/\s/g, "").toLowerCase();
|
|
722
|
+
if (!normalized.includes(fingerprint)) {
|
|
723
|
+
return { removed: true };
|
|
724
|
+
}
|
|
725
|
+
execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
|
|
726
|
+
stdio: "pipe",
|
|
727
|
+
timeout: 3e4
|
|
728
|
+
});
|
|
729
|
+
if (isCATrustedWindows(caCertPath)) {
|
|
730
|
+
return { removed: false, error: "certutil could not remove the portless CA from Root" };
|
|
731
|
+
}
|
|
732
|
+
return { removed: true };
|
|
733
|
+
} catch (err) {
|
|
734
|
+
return { removed: false, error: err instanceof Error ? err.message : String(err) };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
611
737
|
|
|
612
738
|
// src/auto.ts
|
|
613
739
|
import { createHash } from "crypto";
|
|
@@ -783,6 +909,53 @@ function readBranchFromHead(gitdir) {
|
|
|
783
909
|
}
|
|
784
910
|
}
|
|
785
911
|
|
|
912
|
+
// src/clean-utils.ts
|
|
913
|
+
import * as fs3 from "fs";
|
|
914
|
+
import * as path3 from "path";
|
|
915
|
+
var PORTLESS_STATE_FILES = [
|
|
916
|
+
"routes.json",
|
|
917
|
+
"routes.lock",
|
|
918
|
+
"proxy.pid",
|
|
919
|
+
"proxy.port",
|
|
920
|
+
"proxy.log",
|
|
921
|
+
"proxy.tls",
|
|
922
|
+
"proxy.tld",
|
|
923
|
+
"proxy.lan",
|
|
924
|
+
"ca-key.pem",
|
|
925
|
+
"ca.pem",
|
|
926
|
+
"server-key.pem",
|
|
927
|
+
"server.pem",
|
|
928
|
+
"server.csr",
|
|
929
|
+
"server-ext.cnf",
|
|
930
|
+
"ca.srl"
|
|
931
|
+
];
|
|
932
|
+
var HOST_CERTS_DIR2 = "host-certs";
|
|
933
|
+
function collectStateDirsForCleanup() {
|
|
934
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
935
|
+
const add = (d) => {
|
|
936
|
+
const trimmed = d?.trim();
|
|
937
|
+
if (!trimmed) return;
|
|
938
|
+
const resolved = path3.resolve(trimmed);
|
|
939
|
+
if (fs3.existsSync(resolved)) dirs.add(resolved);
|
|
940
|
+
};
|
|
941
|
+
add(USER_STATE_DIR);
|
|
942
|
+
add(SYSTEM_STATE_DIR);
|
|
943
|
+
add(process.env.PORTLESS_STATE_DIR);
|
|
944
|
+
return [...dirs];
|
|
945
|
+
}
|
|
946
|
+
function removePortlessStateFiles(dir) {
|
|
947
|
+
for (const f of PORTLESS_STATE_FILES) {
|
|
948
|
+
try {
|
|
949
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
fs3.rmSync(path3.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
786
959
|
// src/mdns.ts
|
|
787
960
|
import { spawn, spawnSync } from "child_process";
|
|
788
961
|
|
|
@@ -815,7 +988,7 @@ function isInternalInterface(iname, macStr, internal) {
|
|
|
815
988
|
return false;
|
|
816
989
|
}
|
|
817
990
|
function probeDefaultRouteIPv4() {
|
|
818
|
-
return new Promise((
|
|
991
|
+
return new Promise((resolve3, reject) => {
|
|
819
992
|
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
820
993
|
socket.on("error", (error) => {
|
|
821
994
|
socket.close();
|
|
@@ -827,7 +1000,7 @@ function probeDefaultRouteIPv4() {
|
|
|
827
1000
|
socket.close();
|
|
828
1001
|
socket.unref();
|
|
829
1002
|
if (addr && "address" in addr && addr.address && addr.address !== NO_ROUTE_IP) {
|
|
830
|
-
|
|
1003
|
+
resolve3(addr.address);
|
|
831
1004
|
} else {
|
|
832
1005
|
reject(new Error("No route to host"));
|
|
833
1006
|
}
|
|
@@ -1138,10 +1311,10 @@ function getEntryScript() {
|
|
|
1138
1311
|
function isLocallyInstalled() {
|
|
1139
1312
|
let dir = process.cwd();
|
|
1140
1313
|
for (; ; ) {
|
|
1141
|
-
if (
|
|
1314
|
+
if (fs4.existsSync(path4.join(dir, "node_modules", "portless", "package.json"))) {
|
|
1142
1315
|
return true;
|
|
1143
1316
|
}
|
|
1144
|
-
const parent =
|
|
1317
|
+
const parent = path4.dirname(dir);
|
|
1145
1318
|
if (parent === dir) break;
|
|
1146
1319
|
dir = parent;
|
|
1147
1320
|
}
|
|
@@ -1177,11 +1350,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1177
1350
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
1178
1351
|
}
|
|
1179
1352
|
const routesPath = store.getRoutesPath();
|
|
1180
|
-
if (!
|
|
1181
|
-
|
|
1353
|
+
if (!fs4.existsSync(routesPath)) {
|
|
1354
|
+
fs4.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
1182
1355
|
}
|
|
1183
1356
|
try {
|
|
1184
|
-
|
|
1357
|
+
fs4.chmodSync(routesPath, FILE_MODE);
|
|
1185
1358
|
} catch {
|
|
1186
1359
|
}
|
|
1187
1360
|
fixOwnership(routesPath);
|
|
@@ -1189,8 +1362,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1189
1362
|
let debounceTimer = null;
|
|
1190
1363
|
let watcher = null;
|
|
1191
1364
|
let pollingInterval = null;
|
|
1192
|
-
const
|
|
1193
|
-
const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && !activeLanIp && syncVal !== "0" && syncVal !== "false";
|
|
1365
|
+
const autoSyncHosts = shouldAutoSyncHosts(process.env.PORTLESS_SYNC_HOSTS);
|
|
1194
1366
|
const onMdnsError = (msg) => console.warn(chalk.yellow(msg));
|
|
1195
1367
|
const publishCachedRoutes = () => {
|
|
1196
1368
|
if (!activeLanIp) return;
|
|
@@ -1242,7 +1414,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1242
1414
|
}
|
|
1243
1415
|
};
|
|
1244
1416
|
try {
|
|
1245
|
-
watcher =
|
|
1417
|
+
watcher = fs4.watch(routesPath, () => {
|
|
1246
1418
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1247
1419
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
1248
1420
|
});
|
|
@@ -1292,8 +1464,8 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1292
1464
|
redirectServer.listen(80);
|
|
1293
1465
|
}
|
|
1294
1466
|
server.listen(proxyPort, () => {
|
|
1295
|
-
|
|
1296
|
-
|
|
1467
|
+
fs4.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
1468
|
+
fs4.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
1297
1469
|
writeTlsMarker(store.dir, isTls);
|
|
1298
1470
|
writeTldFile(store.dir, tld);
|
|
1299
1471
|
writeLanMarker(store.dir, activeLanIp);
|
|
@@ -1309,7 +1481,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1309
1481
|
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
1310
1482
|
if (isTls) {
|
|
1311
1483
|
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
1312
|
-
console.log(chalk.gray(` ${
|
|
1484
|
+
console.log(chalk.gray(` ${path4.join(store.dir, "ca.pem")}`));
|
|
1313
1485
|
}
|
|
1314
1486
|
if (!lanIpPinned) {
|
|
1315
1487
|
lanMonitor = startLanIpMonitor({
|
|
@@ -1341,11 +1513,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1341
1513
|
redirectServer.close();
|
|
1342
1514
|
}
|
|
1343
1515
|
try {
|
|
1344
|
-
|
|
1516
|
+
fs4.unlinkSync(store.pidPath);
|
|
1345
1517
|
} catch {
|
|
1346
1518
|
}
|
|
1347
1519
|
try {
|
|
1348
|
-
|
|
1520
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1349
1521
|
} catch {
|
|
1350
1522
|
}
|
|
1351
1523
|
writeTlsMarker(store.dir, false);
|
|
@@ -1374,7 +1546,7 @@ function sudoStopOrHint(port) {
|
|
|
1374
1546
|
}
|
|
1375
1547
|
async function stopProxy(store, proxyPort, _tls) {
|
|
1376
1548
|
const pidPath = store.pidPath;
|
|
1377
|
-
if (!
|
|
1549
|
+
if (!fs4.existsSync(pidPath)) {
|
|
1378
1550
|
if (await isProxyRunning(proxyPort)) {
|
|
1379
1551
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
1380
1552
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -1382,7 +1554,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1382
1554
|
try {
|
|
1383
1555
|
process.kill(pid, "SIGTERM");
|
|
1384
1556
|
try {
|
|
1385
|
-
|
|
1557
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1386
1558
|
} catch {
|
|
1387
1559
|
}
|
|
1388
1560
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
@@ -1417,10 +1589,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1417
1589
|
return;
|
|
1418
1590
|
}
|
|
1419
1591
|
try {
|
|
1420
|
-
const pid = parseInt(
|
|
1592
|
+
const pid = parseInt(fs4.readFileSync(pidPath, "utf-8"), 10);
|
|
1421
1593
|
if (isNaN(pid)) {
|
|
1422
1594
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
1423
|
-
|
|
1595
|
+
fs4.unlinkSync(pidPath);
|
|
1424
1596
|
return;
|
|
1425
1597
|
}
|
|
1426
1598
|
try {
|
|
@@ -1431,9 +1603,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1431
1603
|
return;
|
|
1432
1604
|
}
|
|
1433
1605
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
1434
|
-
|
|
1606
|
+
fs4.unlinkSync(pidPath);
|
|
1435
1607
|
try {
|
|
1436
|
-
|
|
1608
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1437
1609
|
} catch {
|
|
1438
1610
|
}
|
|
1439
1611
|
return;
|
|
@@ -1445,13 +1617,13 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1445
1617
|
)
|
|
1446
1618
|
);
|
|
1447
1619
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
1448
|
-
|
|
1620
|
+
fs4.unlinkSync(pidPath);
|
|
1449
1621
|
return;
|
|
1450
1622
|
}
|
|
1451
1623
|
process.kill(pid, "SIGTERM");
|
|
1452
|
-
|
|
1624
|
+
fs4.unlinkSync(pidPath);
|
|
1453
1625
|
try {
|
|
1454
|
-
|
|
1626
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1455
1627
|
} catch {
|
|
1456
1628
|
}
|
|
1457
1629
|
console.log(colors_default.green("Proxy stopped."));
|
|
@@ -1581,10 +1753,10 @@ portless
|
|
|
1581
1753
|
if (!discovered) {
|
|
1582
1754
|
console.error(colors_default.red("Failed to start proxy."));
|
|
1583
1755
|
const fallbackDir = resolveStateDir(getDefaultPort(desiredConfig.useHttps));
|
|
1584
|
-
const logPath =
|
|
1756
|
+
const logPath = path4.join(fallbackDir, "proxy.log");
|
|
1585
1757
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
1586
1758
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1587
|
-
if (
|
|
1759
|
+
if (fs4.existsSync(logPath)) {
|
|
1588
1760
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1589
1761
|
}
|
|
1590
1762
|
process.exit(1);
|
|
@@ -1657,7 +1829,7 @@ portless
|
|
|
1657
1829
|
console.log(chalk.green(` LAN -> ${finalUrl}`));
|
|
1658
1830
|
console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
|
|
1659
1831
|
}
|
|
1660
|
-
const basename3 =
|
|
1832
|
+
const basename3 = path4.basename(commandArgs[0]);
|
|
1661
1833
|
const isExpo = basename3 === "expo";
|
|
1662
1834
|
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
1663
1835
|
const hostBind = isExpoLan ? void 0 : "127.0.0.1";
|
|
@@ -1839,6 +2011,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1839
2011
|
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
1840
2012
|
${colors_default.cyan("portless list")} Show active routes
|
|
1841
2013
|
${colors_default.cyan("portless trust")} Add local CA to system trust store
|
|
2014
|
+
${colors_default.cyan("portless clean")} Remove portless state, trust entry, and hosts block
|
|
1842
2015
|
${colors_default.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
|
|
1843
2016
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1844
2017
|
|
|
@@ -1916,7 +2089,7 @@ ${colors_default.bold("Environment variables:")}
|
|
|
1916
2089
|
PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
|
|
1917
2090
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1918
2091
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1919
|
-
PORTLESS_SYNC_HOSTS=
|
|
2092
|
+
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
|
|
1920
2093
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
1921
2094
|
PORTLESS=0 Run command directly without proxy
|
|
1922
2095
|
|
|
@@ -1929,8 +2102,8 @@ ${colors_default.bold("Child process environment:")}
|
|
|
1929
2102
|
${colors_default.bold("Safari / DNS:")}
|
|
1930
2103
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1931
2104
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1932
|
-
Auto-syncs ${HOSTS_DISPLAY} for
|
|
1933
|
-
|
|
2105
|
+
Auto-syncs ${HOSTS_DISPLAY} for route hostnames by default (including .localhost,
|
|
2106
|
+
custom TLDs, and LAN .local). Set PORTLESS_SYNC_HOSTS=0 to disable. To manually sync:
|
|
1934
2107
|
${colors_default.cyan("portless hosts sync")}
|
|
1935
2108
|
Clean up later with:
|
|
1936
2109
|
${colors_default.cyan("portless hosts clean")}
|
|
@@ -1939,20 +2112,20 @@ ${colors_default.bold("Skip portless:")}
|
|
|
1939
2112
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
1940
2113
|
|
|
1941
2114
|
${colors_default.bold("Reserved names:")}
|
|
1942
|
-
run, get, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
2115
|
+
run, get, alias, hosts, list, trust, clean, proxy are subcommands and cannot
|
|
1943
2116
|
be used as app names directly. Use "portless run" to infer the name,
|
|
1944
2117
|
or "portless --name <name>" to force any name including reserved ones.
|
|
1945
2118
|
`);
|
|
1946
2119
|
process.exit(0);
|
|
1947
2120
|
}
|
|
1948
2121
|
function printVersion() {
|
|
1949
|
-
console.log("0.10.
|
|
2122
|
+
console.log("0.10.1");
|
|
1950
2123
|
process.exit(0);
|
|
1951
2124
|
}
|
|
1952
2125
|
async function handleTrust() {
|
|
1953
2126
|
const { dir } = await discoverState();
|
|
1954
|
-
if (!
|
|
1955
|
-
|
|
2127
|
+
if (!fs4.existsSync(dir)) {
|
|
2128
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1956
2129
|
}
|
|
1957
2130
|
const { caGenerated } = ensureCerts(dir);
|
|
1958
2131
|
if (caGenerated) {
|
|
@@ -1988,6 +2161,90 @@ async function handleTrust() {
|
|
|
1988
2161
|
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1989
2162
|
process.exit(1);
|
|
1990
2163
|
}
|
|
2164
|
+
async function handleClean(args) {
|
|
2165
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
2166
|
+
console.log(`
|
|
2167
|
+
${colors_default.bold("portless clean")} - Remove portless artifacts from this machine.
|
|
2168
|
+
|
|
2169
|
+
Stops the proxy if it is running, removes the local CA from the OS trust store
|
|
2170
|
+
when it was installed by portless, deletes known files under state directories
|
|
2171
|
+
(~/.portless, the system state directory, and PORTLESS_STATE_DIR when set),
|
|
2172
|
+
and removes the portless block from ${HOSTS_DISPLAY}.
|
|
2173
|
+
|
|
2174
|
+
Only allowlisted filenames under each state directory are deleted. Custom
|
|
2175
|
+
certificate paths from --cert and --key are never removed.
|
|
2176
|
+
|
|
2177
|
+
macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
|
|
2178
|
+
require elevated privileges. On Windows, run as Administrator if needed.
|
|
2179
|
+
|
|
2180
|
+
${colors_default.bold("Usage:")}
|
|
2181
|
+
${colors_default.cyan("portless clean")}
|
|
2182
|
+
|
|
2183
|
+
${colors_default.bold("Options:")}
|
|
2184
|
+
--help, -h Show this help
|
|
2185
|
+
`);
|
|
2186
|
+
process.exit(0);
|
|
2187
|
+
}
|
|
2188
|
+
if (args.length > 1) {
|
|
2189
|
+
console.error(colors_default.red(`Error: Unknown argument "${args[1]}".`));
|
|
2190
|
+
console.error(colors_default.cyan(" portless clean --help"));
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
console.log(colors_default.cyan("Stopping proxy if it is running..."));
|
|
2194
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
2195
|
+
const store = new RouteStore(dir, {
|
|
2196
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2197
|
+
});
|
|
2198
|
+
await stopProxy(store, port, tls2);
|
|
2199
|
+
const stateDirs = collectStateDirsForCleanup();
|
|
2200
|
+
for (const stateDir of stateDirs) {
|
|
2201
|
+
const caPath = path4.join(stateDir, "ca.pem");
|
|
2202
|
+
if (!fs4.existsSync(caPath)) continue;
|
|
2203
|
+
const wasTrusted = isCATrusted(stateDir);
|
|
2204
|
+
if (!wasTrusted) continue;
|
|
2205
|
+
const untrustResult = untrustCA(stateDir);
|
|
2206
|
+
if (untrustResult.removed) {
|
|
2207
|
+
console.log(colors_default.green("Removed local CA from the system trust store."));
|
|
2208
|
+
} else if (untrustResult.error) {
|
|
2209
|
+
console.warn(
|
|
2210
|
+
colors_default.yellow(
|
|
2211
|
+
`Could not remove CA from trust store: ${untrustResult.error}
|
|
2212
|
+
Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
for (const stateDir of stateDirs) {
|
|
2218
|
+
removePortlessStateFiles(stateDir);
|
|
2219
|
+
}
|
|
2220
|
+
console.log(colors_default.green("Removed portless state files from known state directories."));
|
|
2221
|
+
if (cleanHostsFile()) {
|
|
2222
|
+
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
2223
|
+
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
2224
|
+
console.log(
|
|
2225
|
+
colors_default.yellow(`Updating ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
2226
|
+
);
|
|
2227
|
+
const result = spawnSync2(
|
|
2228
|
+
"sudo",
|
|
2229
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "clean"],
|
|
2230
|
+
{
|
|
2231
|
+
stdio: "inherit",
|
|
2232
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
2233
|
+
}
|
|
2234
|
+
);
|
|
2235
|
+
if (result.status !== 0) {
|
|
2236
|
+
console.error(colors_default.red(`Failed to update ${HOSTS_DISPLAY}. Run: sudo portless clean`));
|
|
2237
|
+
process.exit(1);
|
|
2238
|
+
}
|
|
2239
|
+
} else {
|
|
2240
|
+
console.warn(
|
|
2241
|
+
colors_default.yellow(
|
|
2242
|
+
`Could not remove portless entries from ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`
|
|
2243
|
+
)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
console.log(colors_default.green("Clean finished."));
|
|
2247
|
+
}
|
|
1991
2248
|
async function handleList() {
|
|
1992
2249
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
1993
2250
|
const store = new RouteStore(dir, {
|
|
@@ -2122,8 +2379,8 @@ ${colors_default.bold("Usage:")}
|
|
|
2122
2379
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
2123
2380
|
|
|
2124
2381
|
${colors_default.bold("Auto-sync:")}
|
|
2125
|
-
|
|
2126
|
-
PORTLESS_SYNC_HOSTS=
|
|
2382
|
+
The proxy updates ${HOSTS_DISPLAY} for route hostnames by default. Disable with
|
|
2383
|
+
PORTLESS_SYNC_HOSTS=0.
|
|
2127
2384
|
`);
|
|
2128
2385
|
process.exit(0);
|
|
2129
2386
|
}
|
|
@@ -2491,8 +2748,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2491
2748
|
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
2492
2749
|
} else {
|
|
2493
2750
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
2494
|
-
const logPath2 =
|
|
2495
|
-
if (
|
|
2751
|
+
const logPath2 = path4.join(resolveStateDir(proxyPort), "proxy.log");
|
|
2752
|
+
if (fs4.existsSync(logPath2)) {
|
|
2496
2753
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
2497
2754
|
}
|
|
2498
2755
|
}
|
|
@@ -2531,8 +2788,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2531
2788
|
store.ensureDir();
|
|
2532
2789
|
if (customCertPath && customKeyPath) {
|
|
2533
2790
|
try {
|
|
2534
|
-
const cert =
|
|
2535
|
-
const key =
|
|
2791
|
+
const cert = fs4.readFileSync(customCertPath);
|
|
2792
|
+
const key = fs4.readFileSync(customKeyPath);
|
|
2536
2793
|
const certStr = cert.toString("utf-8");
|
|
2537
2794
|
const keyStr = key.toString("utf-8");
|
|
2538
2795
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -2577,9 +2834,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2577
2834
|
console.warn(colors_default.cyan(" portless trust"));
|
|
2578
2835
|
}
|
|
2579
2836
|
}
|
|
2580
|
-
const cert =
|
|
2581
|
-
const key =
|
|
2582
|
-
const ca =
|
|
2837
|
+
const cert = fs4.readFileSync(certs.certPath);
|
|
2838
|
+
const key = fs4.readFileSync(certs.keyPath);
|
|
2839
|
+
const ca = fs4.readFileSync(certs.caPath);
|
|
2583
2840
|
tlsOptions = {
|
|
2584
2841
|
cert,
|
|
2585
2842
|
key,
|
|
@@ -2594,11 +2851,11 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2594
2851
|
return;
|
|
2595
2852
|
}
|
|
2596
2853
|
store.ensureDir();
|
|
2597
|
-
const logPath =
|
|
2598
|
-
const logFd =
|
|
2854
|
+
const logPath = path4.join(stateDir, "proxy.log");
|
|
2855
|
+
const logFd = fs4.openSync(logPath, "a");
|
|
2599
2856
|
try {
|
|
2600
2857
|
try {
|
|
2601
|
-
|
|
2858
|
+
fs4.chmodSync(logPath, FILE_MODE);
|
|
2602
2859
|
} catch {
|
|
2603
2860
|
}
|
|
2604
2861
|
fixOwnership(logPath);
|
|
@@ -2628,13 +2885,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2628
2885
|
});
|
|
2629
2886
|
child.unref();
|
|
2630
2887
|
} finally {
|
|
2631
|
-
|
|
2888
|
+
fs4.closeSync(logFd);
|
|
2632
2889
|
}
|
|
2633
2890
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
2634
2891
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
2635
2892
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
2636
2893
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
2637
|
-
if (
|
|
2894
|
+
if (fs4.existsSync(logPath)) {
|
|
2638
2895
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
2639
2896
|
}
|
|
2640
2897
|
process.exit(1);
|
|
@@ -2795,7 +3052,7 @@ async function main() {
|
|
|
2795
3052
|
args.shift();
|
|
2796
3053
|
}
|
|
2797
3054
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
2798
|
-
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
3055
|
+
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean")) {
|
|
2799
3056
|
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
2800
3057
|
if (commandArgs.length === 0) {
|
|
2801
3058
|
console.error(colors_default.red("Error: No command provided."));
|
|
@@ -2817,6 +3074,10 @@ async function main() {
|
|
|
2817
3074
|
await handleTrust();
|
|
2818
3075
|
return;
|
|
2819
3076
|
}
|
|
3077
|
+
if (args[0] === "clean") {
|
|
3078
|
+
await handleClean(args);
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
2820
3081
|
if (args[0] === "list") {
|
|
2821
3082
|
await handleList();
|
|
2822
3083
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -157,6 +157,11 @@ declare function removeBlock(content: string): string;
|
|
|
157
157
|
* Build a portless-managed block for the given hostnames.
|
|
158
158
|
*/
|
|
159
159
|
declare function buildBlock(hostnames: string[]): string;
|
|
160
|
+
/**
|
|
161
|
+
* Whether the proxy should write route hostnames to the hosts file.
|
|
162
|
+
* Disabled only when `PORTLESS_SYNC_HOSTS` is `0` or `false` (opt-out).
|
|
163
|
+
*/
|
|
164
|
+
declare function shouldAutoSyncHosts(syncVal: string | undefined): boolean;
|
|
160
165
|
/**
|
|
161
166
|
* Sync /etc/hosts to include entries for all given hostnames.
|
|
162
167
|
* Replaces any existing portless-managed block. Requires root access.
|
|
@@ -178,4 +183,4 @@ declare function getManagedHostnames(): string[];
|
|
|
178
183
|
*/
|
|
179
184
|
declare function checkHostResolution(hostname: string): Promise<boolean>;
|
|
180
185
|
|
|
181
|
-
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, buildBlock, checkHostResolution, cleanHostsFile, createHttpRedirectServer, createProxyServer, escapeHtml, extractManagedBlock, fixOwnership, formatUrl, getManagedHostnames, isErrnoException, parseHostname, removeBlock, syncHostsFile };
|
|
186
|
+
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, buildBlock, checkHostResolution, cleanHostsFile, createHttpRedirectServer, createProxyServer, escapeHtml, extractManagedBlock, fixOwnership, formatUrl, getManagedHostnames, isErrnoException, parseHostname, removeBlock, shouldAutoSyncHosts, syncHostsFile };
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
isErrnoException,
|
|
20
20
|
parseHostname,
|
|
21
21
|
removeBlock,
|
|
22
|
+
shouldAutoSyncHosts,
|
|
22
23
|
syncHostsFile
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-FM7IA4AF.js";
|
|
24
25
|
export {
|
|
25
26
|
DIR_MODE,
|
|
26
27
|
FILE_MODE,
|
|
@@ -42,5 +43,6 @@ export {
|
|
|
42
43
|
isErrnoException,
|
|
43
44
|
parseHostname,
|
|
44
45
|
removeBlock,
|
|
46
|
+
shouldAutoSyncHosts,
|
|
45
47
|
syncHostsFile
|
|
46
48
|
};
|