portless 0.15.3 → 0.15.4

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +82 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -243,6 +243,8 @@ flowchart TD
243
243
  2. **Run apps**: `portless <name> <command>` assigns a free port and registers with the proxy
244
244
  3. **Access via URL**: `https://<name>.localhost` routes through the proxy to your app
245
245
 
246
+ Outside LAN mode, the proxy and its HTTP redirect listener bind only to the IPv4 and IPv6 loopback addresses, `127.0.0.1` and `::1`. They do not accept connections through LAN, VPN, or other network interfaces.
247
+
246
248
  ## HTTP/2 + HTTPS
247
249
 
248
250
  HTTPS with HTTP/2 is enabled by default. Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers that serve many unbundled files (Vite, Nuxt, etc.). HTTP/2 multiplexes all requests over a single connection.
@@ -287,7 +289,7 @@ portless proxy start --lan --https
287
289
  portless proxy start --lan --ip 192.168.1.42
288
290
  ```
289
291
 
290
- `--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.
292
+ `--lan` explicitly binds the proxy to the IPv4 and IPv6 unspecified addresses, `0.0.0.0` and `::`, and switches to mDNS discovery. This makes services available as `<name>.local` to devices 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.
291
293
 
292
294
  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, TLDs, 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.
293
295
 
package/dist/cli.js CHANGED
@@ -1450,6 +1450,10 @@ var FALLBACK_PROXY_PORT = 1355;
1450
1450
  var PRIVILEGED_PORT_THRESHOLD = 1024;
1451
1451
  var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
1452
1452
  var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
1453
+ var IPV4_LOOPBACK_PROXY_HOST = "127.0.0.1";
1454
+ var IPV6_LOOPBACK_PROXY_HOST = "::1";
1455
+ var IPV4_LAN_PROXY_HOST = "0.0.0.0";
1456
+ var IPV6_LAN_PROXY_HOST = "::";
1453
1457
  var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os2.tmpdir(), "portless") : "/tmp/portless";
1454
1458
  var USER_STATE_DIR = path3.join(resolveUserHome(), ".portless");
1455
1459
  var MIN_APP_PORT = 4e3;
@@ -1552,6 +1556,12 @@ var SIGNAL_CODES = {
1552
1556
  SIGKILL: 9,
1553
1557
  SIGTERM: 15
1554
1558
  };
1559
+ function getProxyBindTargets(lanMode) {
1560
+ return lanMode ? [{ host: IPV4_LAN_PROXY_HOST }, { host: IPV6_LAN_PROXY_HOST, ipv6Only: true }] : [{ host: IPV4_LOOPBACK_PROXY_HOST }, { host: IPV6_LOOPBACK_PROXY_HOST, ipv6Only: true }];
1561
+ }
1562
+ function listenOnProxyInterface(server, port, target, listener) {
1563
+ server.listen({ port, host: target.host, ipv6Only: target.ipv6Only }, listener);
1564
+ }
1555
1565
  function killTree(child, signal = "SIGTERM") {
1556
1566
  if (!child.pid) {
1557
1567
  child.kill(signal);
@@ -4102,6 +4112,12 @@ function formatUrls(hostnames, proxyPort, tls2) {
4102
4112
  function formatViteAllowedHosts(tlds) {
4103
4113
  return tlds.map((configuredTld) => `.${configuredTld}`).join(",");
4104
4114
  }
4115
+ function formatBindEndpoint(host, port) {
4116
+ return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
4117
+ }
4118
+ function isUnavailableIpv6Bind(err, host) {
4119
+ return host.includes(":") && (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
4120
+ }
4105
4121
  function addRoutes(store, hostnames, port, pid, force = false) {
4106
4122
  const registered = [];
4107
4123
  const killedPids = [];
@@ -4137,6 +4153,9 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4137
4153
  const isTls = !!tlsOptions;
4138
4154
  const mdnsSupport = isMdnsSupported();
4139
4155
  let activeLanIp = lanIp && mdnsSupport.supported ? lanIp : null;
4156
+ const lanModeActive = activeLanIp !== null;
4157
+ const bindTargets = getProxyBindTargets(lanModeActive);
4158
+ const primaryBindTarget = bindTargets[0];
4140
4159
  const lanIpPinned = !!process.env.PORTLESS_LAN_IP;
4141
4160
  let lanMonitor = null;
4142
4161
  if (lanIp && !mdnsSupport.supported) {
@@ -4220,7 +4239,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4220
4239
  syncHostsFile(cachedRoutes.map((r) => r.hostname));
4221
4240
  }
4222
4241
  publishCachedRoutes();
4223
- const server = createProxyServer({
4242
+ const createServer2 = () => createProxyServer({
4224
4243
  getRoutes: () => cachedRoutes,
4225
4244
  proxyPort,
4226
4245
  tld,
@@ -4229,6 +4248,17 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4229
4248
  onError: (msg) => console.error(colors_default.red(msg)),
4230
4249
  tls: tlsOptions
4231
4250
  });
4251
+ const server = createServer2();
4252
+ const additionalServers = /* @__PURE__ */ new Set();
4253
+ const redirectServers = /* @__PURE__ */ new Set();
4254
+ const closeAuxiliaryServers = () => {
4255
+ for (const auxiliaryServer of [...additionalServers, ...redirectServers]) {
4256
+ try {
4257
+ auxiliaryServer.close();
4258
+ } catch {
4259
+ }
4260
+ }
4261
+ };
4232
4262
  server.on("error", (err) => {
4233
4263
  if (err.code === "EADDRINUSE") {
4234
4264
  console.error(colors_default.red(`Port ${proxyPort} is already in use.`));
@@ -4247,18 +4277,50 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4247
4277
  } else {
4248
4278
  console.error(colors_default.red(`Proxy error: ${err.message}`));
4249
4279
  }
4250
- if (redirectServer) redirectServer.close();
4280
+ closeAuxiliaryServers();
4251
4281
  process.exit(1);
4252
4282
  });
4253
- let redirectServer = null;
4254
- if (isTls && proxyPort !== 80) {
4255
- redirectServer = createHttpRedirectServer(proxyPort);
4256
- redirectServer.on("error", () => {
4257
- redirectServer = null;
4283
+ const proto = isTls ? "HTTPS/2" : "HTTP";
4284
+ const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
4285
+ const modeLabel = strict === false ? " (wildcard)" : "";
4286
+ for (const bindTarget of bindTargets.slice(1)) {
4287
+ const additionalServer = createServer2();
4288
+ additionalServers.add(additionalServer);
4289
+ additionalServer.on("error", (err) => {
4290
+ additionalServers.delete(additionalServer);
4291
+ if (!isUnavailableIpv6Bind(err, bindTarget.host)) {
4292
+ console.warn(
4293
+ colors_default.yellow(
4294
+ `Could not listen on ${formatBindEndpoint(bindTarget.host, proxyPort)}: ${err.message}`
4295
+ )
4296
+ );
4297
+ }
4298
+ });
4299
+ listenOnProxyInterface(additionalServer, proxyPort, bindTarget, () => {
4300
+ console.log(
4301
+ colors_default.green(
4302
+ `${proto} proxy listening on ${formatBindEndpoint(bindTarget.host, proxyPort)}${tldLabel}${modeLabel}`
4303
+ )
4304
+ );
4258
4305
  });
4259
- redirectServer.listen(80);
4260
4306
  }
4261
- server.listen(proxyPort, () => {
4307
+ if (isTls && proxyPort !== 80) {
4308
+ for (const bindTarget of bindTargets) {
4309
+ const redirectServer = createHttpRedirectServer(proxyPort);
4310
+ redirectServers.add(redirectServer);
4311
+ redirectServer.on("error", () => {
4312
+ redirectServers.delete(redirectServer);
4313
+ });
4314
+ listenOnProxyInterface(redirectServer, 80, bindTarget, () => {
4315
+ console.log(
4316
+ colors_default.green(
4317
+ `HTTP-to-HTTPS redirect listening on ${formatBindEndpoint(bindTarget.host, 80)}`
4318
+ )
4319
+ );
4320
+ });
4321
+ }
4322
+ }
4323
+ listenOnProxyInterface(server, proxyPort, primaryBindTarget, () => {
4262
4324
  fs10.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
4263
4325
  fs10.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
4264
4326
  writeTlsMarker(store.dir, isTls);
@@ -4266,11 +4328,10 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4266
4328
  writeTldsFile(store.dir, tlds);
4267
4329
  writeLanMarker(store.dir, activeLanIp);
4268
4330
  fixOwnership(store.dir, store.pidPath, store.portFilePath);
4269
- const proto = isTls ? "HTTPS/2" : "HTTP";
4270
- const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
4271
- const modeLabel = strict === false ? " (wildcard)" : "";
4272
4331
  console.log(
4273
- colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
4332
+ colors_default.green(
4333
+ `${proto} proxy listening on ${formatBindEndpoint(primaryBindTarget.host, proxyPort)}${tldLabel}${modeLabel}`
4334
+ )
4274
4335
  );
4275
4336
  if (activeLanIp) {
4276
4337
  console.log(chalk.green(`LAN mode: ${activeLanIp}`));
@@ -4290,9 +4351,6 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4290
4351
  });
4291
4352
  }
4292
4353
  }
4293
- if (redirectServer) {
4294
- console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
4295
- }
4296
4354
  });
4297
4355
  let exiting = false;
4298
4356
  const cleanup = () => {
@@ -4305,9 +4363,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4305
4363
  watcher.close();
4306
4364
  }
4307
4365
  if (activeLanIp) cleanupAll();
4308
- if (redirectServer) {
4309
- redirectServer.close();
4310
- }
4366
+ closeAuxiliaryServers();
4311
4367
  try {
4312
4368
  fs10.unlinkSync(store.pidPath);
4313
4369
  } catch {
@@ -5132,6 +5188,7 @@ ${colors_default.bold("How it works:")}
5132
5188
  5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
5133
5189
  Expo, React Native) get --port and, when needed, --host flags
5134
5190
  injected automatically
5191
+ 6. The proxy listens only on 127.0.0.1 and ::1 unless LAN mode is enabled
5135
5192
  Elevated proxy processes keep the invoking user's ~/.portless state directory.
5136
5193
 
5137
5194
  ${colors_default.bold("HTTP/2 + HTTPS (default):")}
@@ -5143,6 +5200,8 @@ ${colors_default.bold("HTTP/2 + HTTPS (default):")}
5143
5200
  ${colors_default.bold("LAN mode:")}
5144
5201
  Use --lan to make services accessible from other devices (phones,
5145
5202
  tablets) on the same WiFi network via mDNS (.local domains).
5203
+ Normal mode binds only to 127.0.0.1 and ::1.
5204
+ LAN mode binds to 0.0.0.0 and ::.
5146
5205
  Useful for testing React Native / Expo apps on real devices.
5147
5206
  Expo keeps Metro's default LAN host behavior in this mode.
5148
5207
  Auto-detected LAN IPs follow network changes automatically.
@@ -5240,7 +5299,7 @@ ${colors_default.bold("Reserved names:")}
5240
5299
  process.exit(0);
5241
5300
  }
5242
5301
  function printVersion() {
5243
- console.log("0.15.3");
5302
+ console.log("0.15.4");
5244
5303
  process.exit(0);
5245
5304
  }
5246
5305
  async function handleTrust() {
@@ -5796,7 +5855,7 @@ ${colors_default.bold("Options:")}
5796
5855
  const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
5797
5856
  const stateExists = fs10.existsSync(state.dir);
5798
5857
  console.log(colors_default.blue.bold("\nportless doctor\n"));
5799
- console.log(`Version: ${"0.15.3"}`);
5858
+ console.log(`Version: ${"0.15.4"}`);
5800
5859
  console.log(`Node.js: ${process.versions.node}`);
5801
5860
  console.log(`Platform: ${process.platform} ${process.arch}`);
5802
5861
  console.log(`State dir: ${state.dir}`);
@@ -6080,6 +6139,8 @@ ${colors_default.bold("Usage:")}
6080
6139
  ${colors_default.cyan("portless proxy stop")} Stop the proxy
6081
6140
 
6082
6141
  ${colors_default.bold("LAN mode (--lan):")}
6142
+ Without LAN mode, the proxy listens only on 127.0.0.1 and ::1.
6143
+ LAN mode explicitly binds the proxy to 0.0.0.0 and ::.
6083
6144
  Makes services accessible from other devices on the same WiFi network
6084
6145
  via mDNS (.local domains). Useful for testing on real mobile devices.
6085
6146
  Auto-detects your LAN IP and follows changes automatically, or use
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portless",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
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",