portless 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  checkHostResolution,
8
8
  cleanHostsFile,
9
9
  createHttpRedirectServer,
10
+ createLoopbackConnection,
10
11
  createProxyServer,
11
12
  fixOwnership,
12
13
  formatUrl,
@@ -14,9 +15,10 @@ import {
14
15
  isErrnoException,
15
16
  isProcessAlive,
16
17
  parseHostname,
18
+ parseHostnames,
17
19
  shouldAutoSyncHosts,
18
20
  syncHostsFile
19
- } from "./chunk-PCBKLZK2.js";
21
+ } from "./chunk-T7CGCIRO.js";
20
22
 
21
23
  // src/colors.ts
22
24
  function supportsColor() {
@@ -501,15 +503,16 @@ async function generateHostCertAsync(stateDir, hostname) {
501
503
  fixOwnership(keyPath, certPath);
502
504
  return { certPath, keyPath };
503
505
  }
504
- function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost", caCert) {
506
+ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost", caCert) {
505
507
  const cache = /* @__PURE__ */ new Map();
506
508
  const pending = /* @__PURE__ */ new Map();
509
+ const configuredTlds = Array.isArray(tlds) ? tlds : [tlds];
507
510
  const defaultCtx = tls.createSecureContext({
508
511
  cert: caCert ? Buffer.concat([defaultCert, caCert]) : defaultCert,
509
512
  key: defaultKey
510
513
  });
511
514
  return (servername, cb) => {
512
- if (servername === tld) {
515
+ if (servername === "localhost" && configuredTlds.includes("localhost")) {
513
516
  cb(null, defaultCtx);
514
517
  return;
515
518
  }
@@ -1256,6 +1259,9 @@ function findGitRoot(startDir) {
1256
1259
  }
1257
1260
  return null;
1258
1261
  }
1262
+ function applyWorktreePrefix(baseName, worktree) {
1263
+ return worktree ? `${worktree.prefix}.${baseName}` : baseName;
1264
+ }
1259
1265
  var DEFAULT_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
1260
1266
  function branchToPrefix(branch) {
1261
1267
  if (!branch || branch === "HEAD" || DEFAULT_BRANCHES.has(branch)) return null;
@@ -1584,8 +1590,25 @@ function validateTld(tld) {
1584
1590
  }
1585
1591
  return null;
1586
1592
  }
1593
+ function parseTldList(value, source = "TLD") {
1594
+ const trimmed = value.trim();
1595
+ if (!trimmed) return [];
1596
+ const tlds = [];
1597
+ const seen = /* @__PURE__ */ new Set();
1598
+ for (const rawPart of trimmed.split(",")) {
1599
+ const tld = rawPart.trim().toLowerCase();
1600
+ const err = validateTld(tld);
1601
+ if (err) throw new Error(source === "TLD" ? err : `${source}: ${err}`);
1602
+ if (!seen.has(tld)) {
1603
+ seen.add(tld);
1604
+ tlds.push(tld);
1605
+ }
1606
+ }
1607
+ return tlds;
1608
+ }
1587
1609
  var TLD_FILE = "proxy.tld";
1588
- function readTldFromDir(dir) {
1610
+ var TLDS_FILE = "proxy.tlds";
1611
+ function readLegacyTldFromDir(dir) {
1589
1612
  try {
1590
1613
  const raw = fs3.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
1591
1614
  return raw || DEFAULT_TLD;
@@ -1593,23 +1616,43 @@ function readTldFromDir(dir) {
1593
1616
  return DEFAULT_TLD;
1594
1617
  }
1595
1618
  }
1596
- function writeTldFile(dir, tld) {
1597
- const filePath = path3.join(dir, TLD_FILE);
1598
- if (tld === DEFAULT_TLD) {
1619
+ function readTldsFromDir(dir) {
1620
+ try {
1621
+ const raw = fs3.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
1622
+ const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
1623
+ if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
1624
+ const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
1625
+ return tlds.length > 0 ? [...new Set(tlds)] : [DEFAULT_TLD];
1626
+ } catch {
1627
+ return [readLegacyTldFromDir(dir)];
1628
+ }
1629
+ }
1630
+ function writeTldsFile(dir, tlds) {
1631
+ const uniqueTlds = [...new Set(tlds)];
1632
+ const tldsPath = path3.join(dir, TLDS_FILE);
1633
+ const tldPath = path3.join(dir, TLD_FILE);
1634
+ if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
1635
+ try {
1636
+ fs3.unlinkSync(tldsPath);
1637
+ } catch {
1638
+ }
1599
1639
  try {
1600
- fs3.unlinkSync(filePath);
1640
+ fs3.unlinkSync(tldPath);
1601
1641
  } catch {
1602
1642
  }
1603
1643
  } else {
1604
- fs3.writeFileSync(filePath, tld, { mode: 420 });
1644
+ fs3.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
1645
+ fs3.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
1605
1646
  }
1606
1647
  }
1607
- function getDefaultTld() {
1648
+ function writeTldFile(dir, tld) {
1649
+ writeTldsFile(dir, [tld]);
1650
+ }
1651
+ function getDefaultTlds() {
1608
1652
  const val = process.env.PORTLESS_TLD?.trim().toLowerCase();
1609
- if (!val) return DEFAULT_TLD;
1610
- const err = validateTld(val);
1611
- if (err) throw new Error(`PORTLESS_TLD: ${err}`);
1612
- return val;
1653
+ if (!val) return [DEFAULT_TLD];
1654
+ const tlds = parseTldList(val, "PORTLESS_TLD");
1655
+ return tlds.length > 0 ? tlds : [DEFAULT_TLD];
1613
1656
  }
1614
1657
  function isHttpsEnvDisabled() {
1615
1658
  const val = process.env.PORTLESS_HTTPS;
@@ -1628,14 +1671,17 @@ function readPersistedProxyState() {
1628
1671
  const port = readPortFromDir(dir);
1629
1672
  if (port !== null) {
1630
1673
  const tls2 = readTlsMarker(dir);
1631
- const tld = readTldFromDir(dir);
1674
+ const tlds = readTldsFromDir(dir);
1675
+ const tld = tlds[0] ?? DEFAULT_TLD;
1632
1676
  const lanIp = readLanMarker(dir);
1633
- return { port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local" };
1677
+ return { port, tls: tls2, tld, tlds, lanMode: lanIp !== null || tlds.includes("local") };
1634
1678
  }
1635
1679
  return null;
1636
1680
  }
1637
1681
  function buildProxyStartConfig(options) {
1638
- const effectiveTld = options.lanMode ? "local" : options.tld;
1682
+ const requestedTlds = options.tlds && options.tlds.length > 0 ? [...options.tlds] : [options.tld];
1683
+ const effectiveTlds = options.lanMode ? ["local"] : [...new Set(requestedTlds)];
1684
+ const effectiveTld = effectiveTlds[0] ?? DEFAULT_TLD;
1639
1685
  const args = [];
1640
1686
  if (options.foreground) {
1641
1687
  args.push("--foreground");
@@ -1661,8 +1707,10 @@ function buildProxyStartConfig(options) {
1661
1707
  args.push(INTERNAL_LAN_IP_FLAG, options.lanIp);
1662
1708
  }
1663
1709
  }
1664
- } else if (effectiveTld !== DEFAULT_TLD) {
1665
- args.push("--tld", effectiveTld);
1710
+ } else if (effectiveTlds.length > 1 || effectiveTld !== DEFAULT_TLD) {
1711
+ for (const tld of effectiveTlds) {
1712
+ args.push("--tld", tld);
1713
+ }
1666
1714
  }
1667
1715
  if (options.useWildcard) {
1668
1716
  args.push("--wildcard");
@@ -1670,7 +1718,7 @@ function buildProxyStartConfig(options) {
1670
1718
  if (options.skipTrust) {
1671
1719
  args.push("--skip-trust");
1672
1720
  }
1673
- return { effectiveTld, args };
1721
+ return { effectiveTld, effectiveTlds, args };
1674
1722
  }
1675
1723
  async function discoverState() {
1676
1724
  if (process.env.PORTLESS_STATE_DIR) {
@@ -1679,14 +1727,25 @@ async function discoverState() {
1679
1727
  const lanIp = readLanMarker(dir2);
1680
1728
  if (await isProxyRunning(port) || await isPortListening(port)) {
1681
1729
  const tls2 = readTlsMarker(dir2);
1682
- const tld = readTldFromDir(dir2);
1683
- return { dir: dir2, port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local", lanIp };
1730
+ const tlds3 = readTldsFromDir(dir2);
1731
+ const tld = tlds3[0] ?? DEFAULT_TLD;
1732
+ return {
1733
+ dir: dir2,
1734
+ port,
1735
+ tls: tls2,
1736
+ tld,
1737
+ tlds: tlds3,
1738
+ lanMode: lanIp !== null || tlds3.includes("local"),
1739
+ lanIp
1740
+ };
1684
1741
  }
1742
+ const tlds2 = readTldsFromDir(dir2);
1685
1743
  return {
1686
1744
  dir: dir2,
1687
1745
  port,
1688
1746
  tls: readTlsMarker(dir2),
1689
- tld: readTldFromDir(dir2),
1747
+ tld: tlds2[0] ?? DEFAULT_TLD,
1748
+ tlds: tlds2,
1690
1749
  lanMode: lanIp !== null,
1691
1750
  lanIp: null
1692
1751
  };
@@ -1695,14 +1754,16 @@ async function discoverState() {
1695
1754
  if (userPort !== null) {
1696
1755
  if (await isProxyRunning(userPort)) {
1697
1756
  const tls2 = readTlsMarker(USER_STATE_DIR);
1698
- const tld = readTldFromDir(USER_STATE_DIR);
1757
+ const tlds2 = readTldsFromDir(USER_STATE_DIR);
1758
+ const tld = tlds2[0] ?? DEFAULT_TLD;
1699
1759
  const lanIp = readLanMarker(USER_STATE_DIR);
1700
1760
  return {
1701
1761
  dir: USER_STATE_DIR,
1702
1762
  port: userPort,
1703
1763
  tls: tls2,
1704
1764
  tld,
1705
- lanMode: lanIp !== null || tld === "local",
1765
+ tlds: tlds2,
1766
+ lanMode: lanIp !== null || tlds2.includes("local"),
1706
1767
  lanIp
1707
1768
  };
1708
1769
  }
@@ -1711,14 +1772,16 @@ async function discoverState() {
1711
1772
  if (legacyPort !== null) {
1712
1773
  if (await isProxyRunning(legacyPort)) {
1713
1774
  const tls2 = readTlsMarker(LEGACY_SYSTEM_STATE_DIR);
1714
- const tld = readTldFromDir(LEGACY_SYSTEM_STATE_DIR);
1775
+ const tlds2 = readTldsFromDir(LEGACY_SYSTEM_STATE_DIR);
1776
+ const tld = tlds2[0] ?? DEFAULT_TLD;
1715
1777
  const lanIp = readLanMarker(LEGACY_SYSTEM_STATE_DIR);
1716
1778
  return {
1717
1779
  dir: LEGACY_SYSTEM_STATE_DIR,
1718
1780
  port: legacyPort,
1719
1781
  tls: tls2,
1720
1782
  tld,
1721
- lanMode: lanIp !== null || tld === "local",
1783
+ tlds: tlds2,
1784
+ lanMode: lanIp !== null || tlds2.includes("local"),
1722
1785
  lanIp
1723
1786
  };
1724
1787
  }
@@ -1730,17 +1793,28 @@ async function discoverState() {
1730
1793
  const dir2 = resolveStateDir(port);
1731
1794
  const markerTls = readTlsMarker(dir2);
1732
1795
  const tls2 = markerTls || port === getProtocolPort(true);
1733
- const tld = readTldFromDir(dir2);
1796
+ const tlds2 = readTldsFromDir(dir2);
1797
+ const tld = tlds2[0] ?? DEFAULT_TLD;
1734
1798
  const lanIp = readLanMarker(dir2);
1735
- return { dir: dir2, port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local", lanIp };
1799
+ return {
1800
+ dir: dir2,
1801
+ port,
1802
+ tls: tls2,
1803
+ tld,
1804
+ tlds: tlds2,
1805
+ lanMode: lanIp !== null || tlds2.includes("local"),
1806
+ lanIp
1807
+ };
1736
1808
  }
1737
1809
  }
1738
1810
  const dir = resolveStateDir(configuredPort);
1811
+ const tlds = readTldsFromDir(dir);
1739
1812
  return {
1740
1813
  dir,
1741
1814
  port: configuredPort,
1742
1815
  tls: readTlsMarker(dir),
1743
- tld: readTldFromDir(dir),
1816
+ tld: tlds[0] ?? DEFAULT_TLD,
1817
+ tlds,
1744
1818
  lanMode: readLanMarker(dir) !== null,
1745
1819
  lanIp: null
1746
1820
  };
@@ -1798,7 +1872,7 @@ function isProxyRunning(port, tls2 = false) {
1798
1872
  }
1799
1873
  function isPortListening(port) {
1800
1874
  return new Promise((resolve4) => {
1801
- const socket = net.createConnection({ host: "127.0.0.1", port });
1875
+ const socket = createLoopbackConnection(port);
1802
1876
  let settled = false;
1803
1877
  const finish = (result) => {
1804
1878
  if (settled) return;
@@ -2025,6 +2099,7 @@ var PORTLESS_STATE_FILES = [
2025
2099
  "proxy.tls",
2026
2100
  "proxy.custom-cert",
2027
2101
  "proxy.tld",
2102
+ "proxy.tlds",
2028
2103
  "proxy.lan",
2029
2104
  "ca-key.pem",
2030
2105
  "ca.pem",
@@ -2797,6 +2872,15 @@ var SYSTEMD_SERVICE = "portless.service";
2797
2872
  var WINDOWS_TASK_NAME = "Portless Proxy";
2798
2873
  var INTERNAL_ELEVATED_ENV = "PORTLESS_INTERNAL_SERVICE_ELEVATED";
2799
2874
  var SERVICE_ENV_KEYS = /* @__PURE__ */ new Set(["PORTLESS_SYNC_HOSTS"]);
2875
+ function normalizeTlds(tlds) {
2876
+ return [...new Set(tlds.length > 0 ? tlds : [DEFAULT_TLD])];
2877
+ }
2878
+ function primaryTld(tlds) {
2879
+ return tlds[0] ?? DEFAULT_TLD;
2880
+ }
2881
+ function formatTldList(tlds) {
2882
+ return tlds.map((tld) => `.${tld}`).join(", ");
2883
+ }
2800
2884
  var DEFAULT_SERVICE_CONFIG = {
2801
2885
  proxyPort: DEFAULT_SERVICE_PORT,
2802
2886
  useHttps: true,
@@ -2806,6 +2890,7 @@ var DEFAULT_SERVICE_CONFIG = {
2806
2890
  lanIp: null,
2807
2891
  lanIpExplicit: false,
2808
2892
  tld: DEFAULT_TLD,
2893
+ tlds: [DEFAULT_TLD],
2809
2894
  useWildcard: false,
2810
2895
  extraEnv: {}
2811
2896
  };
@@ -2892,10 +2977,8 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2892
2977
  config.lanIpExplicit = true;
2893
2978
  }
2894
2979
  if (env.PORTLESS_TLD) {
2895
- const tld = env.PORTLESS_TLD.trim().toLowerCase();
2896
- const err = validateTld(tld);
2897
- if (err) throw new Error(`PORTLESS_TLD: ${err}`);
2898
- config.tld = tld;
2980
+ config.tlds = normalizeTlds(parseTldList(env.PORTLESS_TLD, "PORTLESS_TLD"));
2981
+ config.tld = primaryTld(config.tlds);
2899
2982
  }
2900
2983
  const envWildcard = parseBooleanEnv(env.PORTLESS_WILDCARD);
2901
2984
  if (envWildcard !== null) {
@@ -2907,6 +2990,7 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2907
2990
  config.proxyPort = getProtocolPort(config.useHttps);
2908
2991
  }
2909
2992
  const tokens = args[0] === "service" ? args.slice(2) : args;
2993
+ let tldFlagSeen = false;
2910
2994
  for (let i = 0; i < tokens.length; i += 1) {
2911
2995
  const token = tokens[i];
2912
2996
  switch (token) {
@@ -2931,10 +3015,10 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2931
3015
  i += 1;
2932
3016
  break;
2933
3017
  case "--tld": {
2934
- const tld = getFlagValue(tokens, i, token).trim().toLowerCase();
2935
- const err = validateTld(tld);
2936
- if (err) throw new Error(err);
2937
- config.tld = tld;
3018
+ const tlds = parseTldList(getFlagValue(tokens, i, token));
3019
+ config.tlds = normalizeTlds([...tldFlagSeen ? config.tlds : [], ...tlds]);
3020
+ config.tld = primaryTld(config.tlds);
3021
+ tldFlagSeen = true;
2938
3022
  i += 1;
2939
3023
  break;
2940
3024
  }
@@ -2974,6 +3058,9 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2974
3058
  if (!config.lanMode) {
2975
3059
  config.lanIp = null;
2976
3060
  config.lanIpExplicit = false;
3061
+ } else {
3062
+ config.tlds = ["local"];
3063
+ config.tld = "local";
2977
3064
  }
2978
3065
  return config;
2979
3066
  }
@@ -3019,6 +3106,7 @@ function buildProxyCommand(entryScript, serviceConfig) {
3019
3106
  lanIp: serviceConfig.lanIp,
3020
3107
  lanIpExplicit: serviceConfig.lanIpExplicit,
3021
3108
  tld: serviceConfig.tld,
3109
+ tlds: serviceConfig.tlds,
3022
3110
  useWildcard: serviceConfig.useWildcard,
3023
3111
  foreground: true,
3024
3112
  includePort: true,
@@ -3041,8 +3129,8 @@ function buildServiceEnv(ctx) {
3041
3129
  }
3042
3130
  if (ctx.config.lanMode) {
3043
3131
  env.PORTLESS_TLD = "local";
3044
- } else if (ctx.config.tld !== DEFAULT_TLD) {
3045
- env.PORTLESS_TLD = ctx.config.tld;
3132
+ } else if (ctx.config.tlds.length > 1 || ctx.config.tld !== DEFAULT_TLD) {
3133
+ env.PORTLESS_TLD = ctx.config.tlds.join(",");
3046
3134
  }
3047
3135
  if (ctx.platform === "win32") {
3048
3136
  env.USERPROFILE = ctx.user.home;
@@ -3127,6 +3215,10 @@ function buildServiceSpec(options) {
3127
3215
  ...options.installConfig,
3128
3216
  extraEnv: options.installConfig?.extraEnv ?? {}
3129
3217
  };
3218
+ installConfig.tlds = installConfig.lanMode ? ["local"] : normalizeTlds(
3219
+ options.installConfig?.tlds ?? (options.installConfig?.tld ? [options.installConfig.tld] : installConfig.tlds)
3220
+ );
3221
+ installConfig.tld = primaryTld(installConfig.tlds);
3130
3222
  const stateDir = options.stateDir || installConfig.stateDir || defaultStateDir(options.platform, options.userHome);
3131
3223
  const normalizedConfig = {
3132
3224
  ...installConfig,
@@ -3617,7 +3709,7 @@ async function printServiceStatus(entryScript, runner) {
3617
3709
  ` Proxy on ${config.proxyPort}: ${status.proxyRunning ? "responding" : "not responding"}`
3618
3710
  );
3619
3711
  console.log(` HTTPS: ${config.useHttps ? "yes" : "no"}`);
3620
- console.log(` TLD: ${config.lanMode ? "local" : config.tld}`);
3712
+ console.log(` TLDs: ${config.lanMode ? ".local" : formatTldList(config.tlds)}`);
3621
3713
  console.log(` LAN mode: ${config.lanMode ? "yes" : "no"}`);
3622
3714
  if (config.lanIpExplicit && config.lanIp) {
3623
3715
  console.log(` LAN IP: ${config.lanIp}`);
@@ -3645,7 +3737,7 @@ ${colors_default.bold("Install options:")}
3645
3737
  --https Enable HTTPS
3646
3738
  --lan Enable LAN mode
3647
3739
  --ip <address> Pin a specific LAN IP
3648
- --tld <tld> Use a custom TLD outside LAN mode
3740
+ --tld <tld> Use a custom TLD outside LAN mode, repeatable
3649
3741
  --wildcard Allow subdomain fallback
3650
3742
  --cert <path> Use a custom TLS certificate
3651
3743
  --key <path> Use a custom TLS private key
@@ -3695,7 +3787,17 @@ var DEBOUNCE_MS = 100;
3695
3787
  var POLL_INTERVAL_MS = 3e3;
3696
3788
  var EXIT_TIMEOUT_MS = 2e3;
3697
3789
  var SUDO_SPAWN_TIMEOUT_MS = 3e4;
3698
- function defaultProxyConfig(tld, useHttps, lanMode) {
3790
+ function normalizeTlds2(tlds) {
3791
+ return [...new Set(tlds.length > 0 ? tlds : [DEFAULT_TLD])];
3792
+ }
3793
+ function primaryTld2(tlds) {
3794
+ return tlds[0] ?? DEFAULT_TLD;
3795
+ }
3796
+ function formatTldList2(tlds) {
3797
+ return tlds.map((tld) => `.${tld}`).join(", ");
3798
+ }
3799
+ function defaultProxyConfig(tlds, useHttps, lanMode) {
3800
+ const effectiveTlds = lanMode ? ["local"] : normalizeTlds2(tlds);
3699
3801
  return {
3700
3802
  useHttps,
3701
3803
  customCertPath: null,
@@ -3703,13 +3805,14 @@ function defaultProxyConfig(tld, useHttps, lanMode) {
3703
3805
  lanMode,
3704
3806
  lanIp: null,
3705
3807
  lanIpExplicit: false,
3706
- tld: lanMode ? "local" : tld,
3808
+ tld: primaryTld2(effectiveTlds),
3809
+ tlds: effectiveTlds,
3707
3810
  useWildcard: false
3708
3811
  };
3709
3812
  }
3710
3813
  function resolveProxyConfig(options) {
3711
3814
  const config = defaultProxyConfig(
3712
- options.defaultTld,
3815
+ options.defaultTlds,
3713
3816
  options.useHttps,
3714
3817
  options.explicit.lanMode ? options.lanMode : options.persistedLanMode
3715
3818
  );
@@ -3730,8 +3833,9 @@ function resolveProxyConfig(options) {
3730
3833
  if (!options.lanMode) {
3731
3834
  config.lanIp = null;
3732
3835
  config.lanIpExplicit = false;
3733
- if (!options.explicit.tld) {
3734
- config.tld = options.defaultTld;
3836
+ if (!options.explicit.tlds) {
3837
+ config.tlds = normalizeTlds2(options.defaultTlds);
3838
+ config.tld = primaryTld2(config.tlds);
3735
3839
  }
3736
3840
  }
3737
3841
  }
@@ -3740,8 +3844,9 @@ function resolveProxyConfig(options) {
3740
3844
  config.lanIp = options.lanIp;
3741
3845
  config.lanIpExplicit = true;
3742
3846
  }
3743
- if (options.explicit.tld) {
3744
- config.tld = options.tld;
3847
+ if (options.explicit.tlds) {
3848
+ config.tlds = normalizeTlds2(options.tlds);
3849
+ config.tld = primaryTld2(config.tlds);
3745
3850
  }
3746
3851
  if (options.explicit.useWildcard) {
3747
3852
  config.useWildcard = options.useWildcard;
@@ -3751,6 +3856,7 @@ function resolveProxyConfig(options) {
3751
3856
  config.lanIpExplicit = false;
3752
3857
  }
3753
3858
  if (config.lanMode) {
3859
+ config.tlds = ["local"];
3754
3860
  config.tld = "local";
3755
3861
  if (!config.lanIpExplicit) {
3756
3862
  config.lanIp = null;
@@ -3764,15 +3870,17 @@ function resolveProxyConfig(options) {
3764
3870
  }
3765
3871
  function readCurrentProxyConfig(dir) {
3766
3872
  const lanIp = readLanMarker(dir);
3767
- const tld = readTldFromDir(dir);
3873
+ const tlds = readTldsFromDir(dir);
3874
+ const tld = primaryTld2(tlds);
3768
3875
  return {
3769
3876
  useHttps: readTlsMarker(dir),
3770
3877
  customCertPath: null,
3771
3878
  customKeyPath: null,
3772
- lanMode: lanIp !== null || tld === "local",
3879
+ lanMode: lanIp !== null || tlds.includes("local"),
3773
3880
  lanIp,
3774
3881
  lanIpExplicit: false,
3775
3882
  tld,
3883
+ tlds,
3776
3884
  useWildcard: false
3777
3885
  };
3778
3886
  }
@@ -3793,9 +3901,9 @@ function getProxyConfigMismatchMessages(desiredConfig, actualConfig, explicit) {
3793
3901
  desiredConfig.useHttps ? "requested HTTPS, but the running proxy is using HTTP" : "requested HTTP, but the running proxy is using HTTPS"
3794
3902
  );
3795
3903
  }
3796
- if (explicit.tld && desiredConfig.tld !== actualConfig.tld) {
3904
+ if (explicit.tlds && (desiredConfig.tlds.length !== actualConfig.tlds.length || desiredConfig.tlds.some((tld, index) => tld !== actualConfig.tlds[index]))) {
3797
3905
  messages.push(
3798
- `requested .${desiredConfig.tld}, but the running proxy is using .${actualConfig.tld}`
3906
+ `requested ${formatTldList2(desiredConfig.tlds)}, but the running proxy is using ${formatTldList2(actualConfig.tlds)}`
3799
3907
  );
3800
3908
  }
3801
3909
  return messages;
@@ -3810,6 +3918,7 @@ function formatProxyStartCommand(proxyPort, config) {
3810
3918
  lanIp: config.lanIpExplicit ? config.lanIp : null,
3811
3919
  lanIpExplicit: config.lanIpExplicit,
3812
3920
  tld: config.tld,
3921
+ tlds: config.tlds,
3813
3922
  useWildcard: config.useWildcard,
3814
3923
  includePort: proxyPort !== getDefaultPort(config.useHttps),
3815
3924
  proxyPort
@@ -3903,7 +4012,46 @@ function formatProcessExitSuffix(code, signal) {
3903
4012
  if (code !== null) return ` (exit ${code})`;
3904
4013
  return "";
3905
4014
  }
3906
- function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict, customCert = false) {
4015
+ function buildHostnames(name, tlds) {
4016
+ return parseHostnames(name, normalizeTlds2(tlds));
4017
+ }
4018
+ function formatUrls(hostnames, proxyPort, tls2) {
4019
+ return hostnames.map((hostname) => formatUrl(hostname, proxyPort, tls2));
4020
+ }
4021
+ function formatViteAllowedHosts(tlds) {
4022
+ return tlds.map((configuredTld) => `.${configuredTld}`).join(",");
4023
+ }
4024
+ function addRoutes(store, hostnames, port, pid, force = false) {
4025
+ const registered = [];
4026
+ const killedPids = [];
4027
+ try {
4028
+ for (const hostname of hostnames) {
4029
+ const killedPid = store.addRoute(hostname, port, pid, force);
4030
+ registered.push(hostname);
4031
+ if (killedPid !== void 0) {
4032
+ killedPids.push(killedPid);
4033
+ }
4034
+ }
4035
+ } catch (err) {
4036
+ for (const hostname of registered) {
4037
+ try {
4038
+ store.removeRoute(hostname, pid);
4039
+ } catch {
4040
+ }
4041
+ }
4042
+ throw err;
4043
+ }
4044
+ return [...new Set(killedPids)];
4045
+ }
4046
+ function removeRoutes(store, hostnames, ownerPid) {
4047
+ for (const hostname of hostnames) {
4048
+ try {
4049
+ store.removeRoute(hostname, ownerPid);
4050
+ } catch {
4051
+ }
4052
+ }
4053
+ }
4054
+ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict, customCert = false) {
3907
4055
  store.ensureDir();
3908
4056
  const isTls = !!tlsOptions;
3909
4057
  const mdnsSupport = isMdnsSupported();
@@ -3995,6 +4143,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict, cust
3995
4143
  getRoutes: () => cachedRoutes,
3996
4144
  proxyPort,
3997
4145
  tld,
4146
+ tlds,
3998
4147
  strict,
3999
4148
  onError: (msg) => console.error(colors_default.red(msg)),
4000
4149
  tls: tlsOptions
@@ -4033,11 +4182,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict, cust
4033
4182
  fs9.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
4034
4183
  writeTlsMarker(store.dir, isTls);
4035
4184
  writeCustomCertMarker(store.dir, isTls && customCert);
4036
- writeTldFile(store.dir, tld);
4185
+ writeTldsFile(store.dir, tlds);
4037
4186
  writeLanMarker(store.dir, activeLanIp);
4038
4187
  fixOwnership(store.dir, store.pidPath, store.portFilePath);
4039
4188
  const proto = isTls ? "HTTPS/2" : "HTTP";
4040
- const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
4189
+ const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
4041
4190
  const modeLabel = strict === false ? " (wildcard)" : "";
4042
4191
  console.log(
4043
4192
  colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
@@ -4255,28 +4404,29 @@ function listRoutes(store, proxyPort, tls2) {
4255
4404
  console.log();
4256
4405
  }
4257
4406
  function resolveProxyDesiredState(lanMode) {
4258
- const envTld = getDefaultTld();
4407
+ const envTlds = getDefaultTlds();
4408
+ const envTld = primaryTld2(envTlds);
4259
4409
  const explicit = {
4260
4410
  useHttps: process.env.PORTLESS_HTTPS !== void 0,
4261
4411
  customCert: false,
4262
4412
  lanMode: process.env.PORTLESS_LAN !== void 0,
4263
4413
  lanIp: process.env.PORTLESS_LAN_IP !== void 0,
4264
- tld: process.env.PORTLESS_TLD !== void 0,
4414
+ tlds: process.env.PORTLESS_TLD !== void 0,
4265
4415
  useWildcard: process.env.PORTLESS_WILDCARD !== void 0
4266
4416
  };
4267
4417
  const desiredConfig = resolveProxyConfig({
4268
4418
  persistedLanMode: lanMode,
4269
4419
  explicit,
4270
- defaultTld: envTld,
4420
+ defaultTlds: envTlds,
4271
4421
  useHttps: !isHttpsEnvDisabled(),
4272
4422
  customCertPath: null,
4273
4423
  customKeyPath: null,
4274
4424
  lanMode: isLanEnvEnabled(),
4275
4425
  lanIp: process.env.PORTLESS_LAN_IP || null,
4276
- tld: envTld,
4426
+ tlds: envTlds,
4277
4427
  useWildcard: isWildcardEnvEnabled()
4278
4428
  });
4279
- return { explicit, desiredConfig, envTld };
4429
+ return { explicit, desiredConfig, envTld, envTlds };
4280
4430
  }
4281
4431
  async function ensureProxyRunning(proxyPort, tls2, desired) {
4282
4432
  const { explicit, desiredConfig } = desired;
@@ -4292,8 +4442,9 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4292
4442
  if (!explicit.useHttps && persisted.tls !== desiredConfig.useHttps) {
4293
4443
  startConfig.useHttps = persisted.tls;
4294
4444
  }
4295
- if (!explicit.tld && persisted.tld !== desiredConfig.tld) {
4296
- startConfig.tld = persisted.tld;
4445
+ if (!explicit.tlds && (persisted.tlds.length !== desiredConfig.tlds.length || persisted.tlds.some((tld, index) => tld !== desiredConfig.tlds[index]))) {
4446
+ startConfig.tlds = persisted.tlds;
4447
+ startConfig.tld = primaryTld2(persisted.tlds);
4297
4448
  }
4298
4449
  if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
4299
4450
  startConfig.lanMode = persisted.lanMode;
@@ -4329,6 +4480,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4329
4480
  lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
4330
4481
  lanIpExplicit: startConfig.lanIpExplicit,
4331
4482
  tld: startConfig.tld,
4483
+ tlds: startConfig.tlds,
4332
4484
  useWildcard: startConfig.useWildcard,
4333
4485
  includePort: startPort !== void 0,
4334
4486
  proxyPort: startPort
@@ -4363,7 +4515,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4363
4515
  }
4364
4516
  return { started: true, state: discovered };
4365
4517
  }
4366
- async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort, lanMode = false, lanIp) {
4518
+ async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tlds, force, autoInfo, desiredPort, lanMode = false, lanIp) {
4367
4519
  let store = initialStore;
4368
4520
  console.log(chalk.blue.bold(`
4369
4521
  portless
@@ -4410,12 +4562,12 @@ portless
4410
4562
  console.error(colors_default.red(`Error: ${err.message}`));
4411
4563
  process.exit(1);
4412
4564
  }
4413
- parseHostname(name, tld);
4565
+ buildHostnames(name, tlds);
4414
4566
  const ensureResult = await ensureProxyRunning(proxyPort, tls2, desired);
4415
4567
  if (ensureResult.started) {
4416
4568
  proxyPort = ensureResult.state.port;
4417
4569
  stateDir = ensureResult.state.dir;
4418
- tld = ensureResult.state.tld;
4570
+ tlds = ensureResult.state.tlds;
4419
4571
  tls2 = ensureResult.state.tls;
4420
4572
  lanMode = ensureResult.state.lanMode;
4421
4573
  lanIp = ensureResult.state.lanIp;
@@ -4437,20 +4589,22 @@ portless
4437
4589
  }
4438
4590
  lanMode = runningConfig.lanMode;
4439
4591
  lanIp = runningConfig.lanIp;
4592
+ tlds = runningConfig.tlds;
4440
4593
  console.log(chalk.gray("-- Proxy is running"));
4441
4594
  }
4442
- const hostname = parseHostname(name, tld);
4443
- if (desired.envTld !== DEFAULT_TLD && desired.envTld !== tld) {
4595
+ const hostnames = buildHostnames(name, tlds);
4596
+ const hostname = hostnames[0];
4597
+ if (desired.explicit.tlds && (desired.envTlds.some((envConfiguredTld, index) => envConfiguredTld !== tlds[index]) || desired.envTlds.length !== tlds.length)) {
4444
4598
  console.warn(
4445
4599
  chalk.yellow(
4446
- `Warning: PORTLESS_TLD=${desired.envTld} but the running proxy uses .${tld}. Using .${tld}.`
4600
+ `Warning: PORTLESS_TLD=${desired.envTlds.join(",")} but the running proxy uses ${formatTldList2(tlds)}.`
4447
4601
  )
4448
4602
  );
4449
4603
  }
4450
4604
  if (lanIp) {
4451
- console.log(chalk.gray(`-- ${hostname} (LAN: ${lanIp})`));
4605
+ console.log(chalk.gray(`-- ${hostnames.join(", ")} (LAN: ${lanIp})`));
4452
4606
  } else {
4453
- console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
4607
+ console.log(chalk.gray(`-- ${hostnames.join(", ")} (auto-resolves to 127.0.0.1)`));
4454
4608
  }
4455
4609
  if (autoInfo) {
4456
4610
  const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
@@ -4465,9 +4619,9 @@ portless
4465
4619
  } else {
4466
4620
  console.log(colors_default.green(`-- Using port ${port}`));
4467
4621
  }
4468
- let killedPid;
4622
+ let killedPids = [];
4469
4623
  try {
4470
- killedPid = store.addRoute(hostname, port, process.pid, force);
4624
+ killedPids = addRoutes(store, hostnames, port, process.pid, force);
4471
4625
  } catch (err) {
4472
4626
  if (err instanceof RouteConflictError) {
4473
4627
  console.error(colors_default.red(`Error: ${err.message}`));
@@ -4475,13 +4629,20 @@ portless
4475
4629
  }
4476
4630
  throw err;
4477
4631
  }
4478
- if (killedPid !== void 0) {
4479
- console.log(colors_default.yellow(`Killed existing process (PID ${killedPid})`));
4632
+ if (killedPids.length > 0) {
4633
+ console.log(colors_default.yellow(`Killed existing process(es): ${killedPids.join(", ")}`));
4480
4634
  }
4481
4635
  const finalUrl = formatUrl(hostname, proxyPort, tls2);
4636
+ const allUrls = formatUrls(hostnames, proxyPort, tls2);
4482
4637
  console.log(chalk.cyan.bold(`
4483
4638
  -> ${finalUrl}
4484
4639
  `));
4640
+ for (const extraUrl of allUrls.slice(1)) {
4641
+ console.log(chalk.cyan(` also -> ${extraUrl}`));
4642
+ }
4643
+ if (allUrls.length > 1) {
4644
+ console.log("");
4645
+ }
4485
4646
  if (lanIp) {
4486
4647
  console.log(chalk.green(` LAN -> ${finalUrl}`));
4487
4648
  console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
@@ -4594,7 +4755,7 @@ portless
4594
4755
  } catch {
4595
4756
  }
4596
4757
  try {
4597
- store.removeRoute(hostname, process.pid);
4758
+ removeRoutes(store, hostnames, process.pid);
4598
4759
  } catch {
4599
4760
  }
4600
4761
  process.exit(1);
@@ -4628,7 +4789,7 @@ portless
4628
4789
  PORT: port.toString(),
4629
4790
  ...hostBind ? { HOST: hostBind } : {},
4630
4791
  PORTLESS_URL: finalUrl,
4631
- __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`,
4792
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds),
4632
4793
  // Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
4633
4794
  // baked-in pinging, making this env var ineffective. Expo handles its
4634
4795
  // own LAN discovery natively.
@@ -4648,7 +4809,7 @@ portless
4648
4809
  } catch {
4649
4810
  }
4650
4811
  try {
4651
- store.removeRoute(hostname, process.pid);
4812
+ removeRoutes(store, hostnames, process.pid);
4652
4813
  } catch {
4653
4814
  }
4654
4815
  }
@@ -4941,7 +5102,7 @@ ${colors_default.bold("Options:")}
4941
5102
  --cert <path> Use a custom TLS certificate
4942
5103
  --key <path> Use a custom TLS private key
4943
5104
  --foreground Run proxy in foreground (for debugging)
4944
- --tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
5105
+ --tld <tld> Use a custom TLD instead of .localhost; repeat for more
4945
5106
  --wildcard Allow unregistered subdomains to fall back to parent route
4946
5107
  --state-dir <path> Use a custom state directory with service install
4947
5108
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
@@ -4958,7 +5119,7 @@ ${colors_default.bold("Environment variables:")}
4958
5119
  PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
4959
5120
  PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
4960
5121
  PORTLESS_LAN_IP=<address> Pin a specific LAN IP for LAN mode
4961
- PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
5122
+ PORTLESS_TLD=<tld>[,<tld>] Use one or more TLDs (e.g. localhost,test)
4962
5123
  PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
4963
5124
  PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
4964
5125
  PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
@@ -4970,7 +5131,7 @@ ${colors_default.bold("Environment variables:")}
4970
5131
  ${colors_default.bold("Child process environment:")}
4971
5132
  PORT Ephemeral port the child should listen on
4972
5133
  HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
4973
- PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
5134
+ PORTLESS_URL Primary public URL of the app
4974
5135
  PORTLESS_LAN Set to 1 when proxy is in LAN mode
4975
5136
  PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
4976
5137
  PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
@@ -4996,7 +5157,7 @@ ${colors_default.bold("Reserved names:")}
4996
5157
  process.exit(0);
4997
5158
  }
4998
5159
  function printVersion() {
4999
- console.log("0.15.0");
5160
+ console.log("0.15.2");
5000
5161
  process.exit(0);
5001
5162
  }
5002
5163
  async function handleTrust() {
@@ -5271,8 +5432,8 @@ ${colors_default.bold("Examples:")}
5271
5432
  const name = positional[0];
5272
5433
  const worktree = skipWorktree ? null : detectWorktreePrefix();
5273
5434
  const effectiveName = worktree ? `${worktree.prefix}.${name}` : name;
5274
- const { port, tls: tls2, tld } = await discoverState();
5275
- const hostname = parseHostname(effectiveName, tld);
5435
+ const { port, tls: tls2, tlds } = await discoverState();
5436
+ const hostname = buildHostnames(effectiveName, tlds)[0];
5276
5437
  const url = formatUrl(hostname, port, tls2);
5277
5438
  process.stdout.write(url + "\n");
5278
5439
  }
@@ -5293,7 +5454,7 @@ ${colors_default.bold("Examples:")}
5293
5454
  `);
5294
5455
  process.exit(0);
5295
5456
  }
5296
- const { dir, tld } = await discoverState();
5457
+ const { dir, tlds } = await discoverState();
5297
5458
  const store = new RouteStore(dir, {
5298
5459
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
5299
5460
  });
@@ -5304,15 +5465,15 @@ ${colors_default.bold("Examples:")}
5304
5465
  console.error(colors_default.cyan(" portless alias --remove <name>"));
5305
5466
  process.exit(1);
5306
5467
  }
5307
- const hostname2 = parseHostname(aliasName2, tld);
5468
+ const hostnames2 = buildHostnames(aliasName2, tlds);
5308
5469
  const routes = store.loadRoutes();
5309
- const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
5470
+ const existing = routes.find((r) => hostnames2.includes(r.hostname) && r.pid === 0);
5310
5471
  if (!existing) {
5311
- console.error(colors_default.red(`Error: No alias found for "${hostname2}".`));
5472
+ console.error(colors_default.red(`Error: No alias found for "${hostnames2.join(", ")}".`));
5312
5473
  process.exit(1);
5313
5474
  }
5314
- store.removeRoute(hostname2);
5315
- console.log(colors_default.green(`Removed alias: ${hostname2}`));
5475
+ removeRoutes(store, hostnames2);
5476
+ console.log(colors_default.green(`Removed alias: ${hostnames2.join(", ")}`));
5316
5477
  return;
5317
5478
  }
5318
5479
  const aliasName = args[1];
@@ -5326,15 +5487,15 @@ ${colors_default.bold("Examples:")}
5326
5487
  console.error(colors_default.cyan(" portless alias my-postgres 5432"));
5327
5488
  process.exit(1);
5328
5489
  }
5329
- const hostname = parseHostname(aliasName, tld);
5490
+ const hostnames = buildHostnames(aliasName, tlds);
5330
5491
  const port = parseInt(aliasPort, 10);
5331
5492
  if (isNaN(port) || port < 1 || port > 65535) {
5332
5493
  console.error(colors_default.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
5333
5494
  process.exit(1);
5334
5495
  }
5335
5496
  const force = args.includes("--force");
5336
- store.addRoute(hostname, port, 0, force);
5337
- console.log(colors_default.green(`Alias registered: ${hostname} -> 127.0.0.1:${port}`));
5497
+ addRoutes(store, hostnames, port, 0, force);
5498
+ console.log(colors_default.green(`Alias registered: ${hostnames.join(", ")} -> 127.0.0.1:${port}`));
5338
5499
  }
5339
5500
  async function handleHosts(args) {
5340
5501
  if (args[1] === "--help" || args[1] === "-h") {
@@ -5524,6 +5685,7 @@ ${colors_default.bold("Options:")}
5524
5685
  port: getDefaultPort(!isHttpsEnvDisabled()),
5525
5686
  tls: !isHttpsEnvDisabled(),
5526
5687
  tld: DEFAULT_TLD,
5688
+ tlds: [DEFAULT_TLD],
5527
5689
  lanMode: isLanEnvEnabled(),
5528
5690
  lanIp: null
5529
5691
  };
@@ -5545,12 +5707,14 @@ ${colors_default.bold("Options:")}
5545
5707
  const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
5546
5708
  const stateExists = fs9.existsSync(state.dir);
5547
5709
  console.log(colors_default.blue.bold("\nportless doctor\n"));
5548
- console.log(`Version: ${"0.15.0"}`);
5710
+ console.log(`Version: ${"0.15.2"}`);
5549
5711
  console.log(`Node.js: ${process.versions.node}`);
5550
5712
  console.log(`Platform: ${process.platform} ${process.arch}`);
5551
5713
  console.log(`State dir: ${state.dir}`);
5552
5714
  console.log(`Proxy target: ${formatUrl("127.0.0.1", proxyPort, proxyTls)}`);
5553
- console.log(`Mode: ${proxyTls ? "HTTPS" : "HTTP"}, .${state.tld}${state.lanMode ? ", LAN" : ""}`);
5715
+ console.log(
5716
+ `Mode: ${proxyTls ? "HTTPS" : "HTTP"}, ${formatTldList2(state.tlds)}${state.lanMode ? ", LAN" : ""}`
5717
+ );
5554
5718
  console.log("");
5555
5719
  const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
5556
5720
  if (nodeMajor >= 24) {
@@ -5822,6 +5986,7 @@ ${colors_default.bold("Usage:")}
5822
5986
  ${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
5823
5987
  ${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
5824
5988
  ${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
5989
+ ${colors_default.cyan("portless proxy start --tld localhost --tld test")} Serve both TLDs
5825
5990
  ${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
5826
5991
  ${colors_default.cyan("portless proxy stop")} Stop the proxy
5827
5992
 
@@ -5883,34 +6048,41 @@ ${colors_default.bold("LAN mode (--lan):")}
5883
6048
  }
5884
6049
  hasExplicitPort = true;
5885
6050
  }
5886
- let tld;
6051
+ let tlds;
5887
6052
  try {
5888
- tld = getDefaultTld();
6053
+ tlds = getDefaultTlds();
5889
6054
  } catch (err) {
5890
6055
  console.error(colors_default.red(`Error: ${err.message}`));
5891
6056
  process.exit(1);
5892
6057
  }
5893
- const tldIdx = args.indexOf("--tld");
5894
- if (tldIdx !== -1) {
5895
- const tldValue = args[tldIdx + 1];
5896
- if (!tldValue || tldValue.startsWith("-")) {
5897
- console.error(colors_default.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
5898
- process.exit(1);
6058
+ const tldFlagValues = [];
6059
+ for (let i = 0; i < args.length; i += 1) {
6060
+ if (args[i] === "--tld") {
6061
+ const tldValue = args[i + 1];
6062
+ if (!tldValue || tldValue.startsWith("-")) {
6063
+ console.error(colors_default.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
6064
+ process.exit(1);
6065
+ }
6066
+ tldFlagValues.push(tldValue);
6067
+ i += 1;
5899
6068
  }
5900
- tld = tldValue.trim().toLowerCase();
5901
- const tldErr = validateTld(tld);
5902
- if (tldErr) {
5903
- console.error(colors_default.red(`Error: ${tldErr}`));
6069
+ }
6070
+ if (tldFlagValues.length > 0) {
6071
+ try {
6072
+ tlds = normalizeTlds2(tldFlagValues.flatMap((value) => parseTldList(value)));
6073
+ } catch (err) {
6074
+ console.error(colors_default.red(`Error: ${err.message}`));
5904
6075
  process.exit(1);
5905
6076
  }
5906
6077
  }
6078
+ let tld = primaryTld2(tlds);
5907
6079
  const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
5908
6080
  const explicit = {
5909
6081
  useHttps: hasHttpsFlag || hasNoTls || customCertPath !== null || customKeyPath !== null || process.env.PORTLESS_HTTPS !== void 0,
5910
6082
  customCert: customCertPath !== null || customKeyPath !== null,
5911
6083
  lanMode: process.env.PORTLESS_LAN !== void 0,
5912
6084
  lanIp: process.env.PORTLESS_LAN_IP !== void 0,
5913
- tld: tldIdx !== -1 || process.env.PORTLESS_TLD !== void 0,
6085
+ tlds: tldFlagValues.length > 0 || process.env.PORTLESS_TLD !== void 0,
5914
6086
  useWildcard: args.includes("--wildcard") || process.env.PORTLESS_WILDCARD !== void 0
5915
6087
  };
5916
6088
  let stateDir = resolveStateDir(proxyPort);
@@ -5928,13 +6100,13 @@ ${colors_default.bold("LAN mode (--lan):")}
5928
6100
  const desiredConfig = resolveProxyConfig({
5929
6101
  persistedLanMode,
5930
6102
  explicit,
5931
- defaultTld: getDefaultTld(),
6103
+ defaultTlds: getDefaultTlds(),
5932
6104
  useHttps: wantHttps || !!(customCertPath && customKeyPath),
5933
6105
  customCertPath,
5934
6106
  customKeyPath,
5935
6107
  lanMode: isLanEnvEnabled(),
5936
6108
  lanIp: process.env.PORTLESS_LAN_IP || null,
5937
- tld,
6109
+ tlds,
5938
6110
  useWildcard
5939
6111
  });
5940
6112
  const lanMode = desiredConfig.lanMode;
@@ -5942,31 +6114,35 @@ ${colors_default.bold("LAN mode (--lan):")}
5942
6114
  customCertPath = desiredConfig.customCertPath;
5943
6115
  customKeyPath = desiredConfig.customKeyPath;
5944
6116
  tld = desiredConfig.tld;
6117
+ tlds = desiredConfig.tlds;
5945
6118
  const desiredWildcard = desiredConfig.useWildcard;
5946
6119
  let lanIp = desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null;
5947
6120
  if (!hasExplicitPort && runningPort === null) {
5948
6121
  proxyPort = getDefaultPort(useHttps);
5949
6122
  stateDir = resolveStateDir(proxyPort);
5950
6123
  }
5951
- if (lanMode && tldIdx !== -1) {
5952
- const userTld = args[tldIdx + 1];
5953
- if (userTld && userTld !== "local") {
6124
+ if (lanMode && tldFlagValues.length > 0) {
6125
+ const userTlds = tldFlagValues.join(", ");
6126
+ if (userTlds !== "local") {
5954
6127
  console.warn(
5955
6128
  chalk.yellow(
5956
- `Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTld}.`
6129
+ `Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTlds}.`
5957
6130
  )
5958
6131
  );
5959
6132
  }
5960
6133
  }
5961
- const riskyReason = RISKY_TLDS.get(tld);
5962
- if (riskyReason && !lanMode) {
5963
- console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
6134
+ for (const configuredTld of tlds) {
6135
+ const riskyReason = RISKY_TLDS.get(configuredTld);
6136
+ if (riskyReason && !lanMode) {
6137
+ console.warn(colors_default.yellow(`Warning: .${configuredTld}: ${riskyReason}`));
6138
+ }
5964
6139
  }
5965
6140
  const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
5966
- if (tld !== DEFAULT_TLD && !lanMode && syncDisabled) {
6141
+ const nonDefaultTlds = tlds.filter((configuredTld) => configuredTld !== DEFAULT_TLD);
6142
+ if (nonDefaultTlds.length > 0 && !lanMode && syncDisabled) {
5967
6143
  console.warn(
5968
6144
  colors_default.yellow(
5969
- `Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
6145
+ `Warning: ${formatTldList2(nonDefaultTlds)} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
5970
6146
  )
5971
6147
  );
5972
6148
  console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
@@ -6028,6 +6204,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6028
6204
  lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
6029
6205
  lanIpExplicit: desiredConfig.lanIpExplicit,
6030
6206
  tld,
6207
+ tlds,
6031
6208
  useWildcard: desiredWildcard
6032
6209
  };
6033
6210
  if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
@@ -6044,6 +6221,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6044
6221
  lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
6045
6222
  lanIpExplicit: desiredConfig.lanIpExplicit,
6046
6223
  tld,
6224
+ tlds,
6047
6225
  useWildcard: desiredWildcard,
6048
6226
  foreground: isForeground,
6049
6227
  includePort: true,
@@ -6161,7 +6339,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6161
6339
  cert,
6162
6340
  key,
6163
6341
  ca,
6164
- SNICallback: createSNICallback(stateDir, cert, key, tld, ca)
6342
+ SNICallback: createSNICallback(stateDir, cert, key, tlds, ca)
6165
6343
  };
6166
6344
  }
6167
6345
  }
@@ -6171,6 +6349,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6171
6349
  store,
6172
6350
  proxyPort,
6173
6351
  tld,
6352
+ tlds,
6174
6353
  tlsOptions,
6175
6354
  lanIp,
6176
6355
  desiredWildcard ? false : void 0,
@@ -6199,6 +6378,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6199
6378
  lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
6200
6379
  lanIpExplicit: desiredConfig.lanIpExplicit,
6201
6380
  tld,
6381
+ tlds,
6202
6382
  useWildcard: desiredWildcard,
6203
6383
  foreground: true,
6204
6384
  includePort: true,
@@ -6291,8 +6471,8 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
6291
6471
  nameSource = inferred.source;
6292
6472
  }
6293
6473
  const worktree = detectWorktreePrefix(cwd);
6294
- const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
6295
- const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
6474
+ const effectiveName = applyWorktreePrefix(baseName, worktree);
6475
+ const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6296
6476
  const store = new RouteStore(dir, {
6297
6477
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6298
6478
  });
@@ -6303,7 +6483,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
6303
6483
  effectiveName,
6304
6484
  resolved,
6305
6485
  tls2,
6306
- tld,
6486
+ tlds,
6307
6487
  false,
6308
6488
  { nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
6309
6489
  appConfig?.appPort,
@@ -6343,13 +6523,13 @@ function pipeOutput(child, prefix) {
6343
6523
  prefixStream(child.stdout, process.stdout, prefix);
6344
6524
  prefixStream(child.stderr, process.stderr, prefix);
6345
6525
  }
6346
- async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6526
+ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes) {
6347
6527
  const usesPortless = app.commandArgs[0] === "portless";
6348
6528
  const pkgEnv = { ...process.env };
6349
6529
  pkgEnv.PATH = augmentedPath(pkgEnv, app.pkg.dir);
6350
6530
  let env;
6351
6531
  let store = null;
6352
- let hostname = null;
6532
+ let hostnames = [];
6353
6533
  let displayUrl;
6354
6534
  if (usesPortless) {
6355
6535
  env = pkgEnv;
@@ -6359,17 +6539,17 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6359
6539
  onWarning: (msg) => console.warn(colors_default.yellow(`[${app.name}] ${msg}`))
6360
6540
  });
6361
6541
  const appPort = app.appPort ?? await findFreePort();
6362
- const protocol = tls2 ? "https" : "http";
6363
- const portSuffix = tls2 && proxyPort === 443 || !tls2 && proxyPort === 80 ? "" : `:${proxyPort}`;
6364
- const url = `${protocol}://${app.name}.${tld}${portSuffix}`;
6542
+ hostnames = buildHostnames(app.name, tlds);
6543
+ const urls = formatUrls(hostnames, proxyPort, tls2);
6544
+ const url = urls[0];
6365
6545
  displayUrl = url;
6366
- hostname = parseHostname(app.name, tld);
6367
- store.addRoute(hostname, appPort, process.pid);
6546
+ addRoutes(store, hostnames, appPort, process.pid);
6368
6547
  env = {
6369
6548
  ...pkgEnv,
6370
6549
  PORT: String(appPort),
6371
6550
  HOST: "127.0.0.1",
6372
- PORTLESS_URL: url
6551
+ PORTLESS_URL: url,
6552
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds)
6373
6553
  };
6374
6554
  if (tls2) {
6375
6555
  const caPath = path9.join(stateDir, "ca.pem");
@@ -6381,7 +6561,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6381
6561
  const child = spawnChildProcess(app.commandArgs, env, app.pkg.dir);
6382
6562
  pipeOutput(child, chalk.cyan(`[${app.name}]`));
6383
6563
  const capturedStore = store;
6384
- const capturedHostname = hostname;
6564
+ const capturedHostnames = hostnames;
6385
6565
  child.on("exit", (code, signal) => {
6386
6566
  exitCodes.set(app.name, code);
6387
6567
  if (code !== 0 && code !== null) {
@@ -6389,14 +6569,11 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6389
6569
  } else if (signal) {
6390
6570
  console.error(colors_default.yellow(`[${app.name}] killed by ${signal}`));
6391
6571
  }
6392
- if (capturedStore && capturedHostname) {
6393
- try {
6394
- capturedStore.removeRoute(capturedHostname, process.pid);
6395
- } catch {
6396
- }
6572
+ if (capturedStore && capturedHostnames.length > 0) {
6573
+ removeRoutes(capturedStore, capturedHostnames, process.pid);
6397
6574
  }
6398
6575
  });
6399
- const route = store && hostname ? { store, hostname } : null;
6576
+ const route = store && hostnames.length > 0 ? { store, hostnames } : null;
6400
6577
  return { child, displayUrl, route };
6401
6578
  }
6402
6579
  function spawnTaskApp(app, exitCodes) {
@@ -6454,6 +6631,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
6454
6631
  }
6455
6632
  }
6456
6633
  const apps = [];
6634
+ const worktree = detectWorktreePrefix(wsRoot);
6457
6635
  for (const pkg of packages) {
6458
6636
  const rel = path9.relative(wsRoot, pkg.dir).replace(/\\/g, "/");
6459
6637
  const rootOverride = loaded ? resolveAppConfig(loaded.config, loaded.configDir, pkg.dir) : null;
@@ -6495,6 +6673,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
6495
6673
  name = pkgLabel === projectName ? projectName : `${pkgLabel}.${projectName}`;
6496
6674
  label = pkg.scope ? `@${pkg.scope}/${pkg.name}` : pkg.name ?? rel;
6497
6675
  }
6676
+ name = applyWorktreePrefix(name, worktree);
6498
6677
  apps.push({ pkg, name, label, commandArgs, appPort: appOverride.appPort, proxied });
6499
6678
  }
6500
6679
  if (apps.length === 0) {
@@ -6507,7 +6686,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
6507
6686
  console.log(chalk.blue.bold(`
6508
6687
  portless
6509
6688
  `));
6510
- let { dir, port, tls: tls2, tld } = await discoverState();
6689
+ let { dir, port, tls: tls2, tlds } = await discoverState();
6511
6690
  if (proxiedApps.length > 0) {
6512
6691
  let multiDesired;
6513
6692
  try {
@@ -6521,9 +6700,9 @@ portless
6521
6700
  dir = ensureResult.state.dir;
6522
6701
  port = ensureResult.state.port;
6523
6702
  tls2 = ensureResult.state.tls;
6524
- tld = ensureResult.state.tld;
6703
+ tlds = ensureResult.state.tlds;
6525
6704
  } else {
6526
- ({ dir, port, tls: tls2, tld } = await discoverState());
6705
+ ({ dir, port, tls: tls2, tlds } = await discoverState());
6527
6706
  }
6528
6707
  if (tls2 && !isCATrusted(dir)) {
6529
6708
  await handleTrust();
@@ -6531,12 +6710,12 @@ portless
6531
6710
  }
6532
6711
  const useTurbo = loaded?.config.turbo !== false && hasTurboConfig(wsRoot);
6533
6712
  if (useTurbo) {
6534
- await runWithTurbo(wsRoot, dir, port, tls2, tld, scriptName, proxiedApps, taskApps, extraArgs);
6713
+ await runWithTurbo(wsRoot, dir, port, tls2, tlds, scriptName, proxiedApps, taskApps, extraArgs);
6535
6714
  } else {
6536
- await runWithDirectSpawn(dir, port, tls2, tld, proxiedApps, taskApps);
6715
+ await runWithDirectSpawn(dir, port, tls2, tlds, proxiedApps, taskApps);
6537
6716
  }
6538
6717
  }
6539
- async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName, proxiedApps, taskApps, extraArgs = []) {
6718
+ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName, proxiedApps, taskApps, extraArgs = []) {
6540
6719
  const store = new RouteStore(stateDir, {
6541
6720
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6542
6721
  });
@@ -6550,17 +6729,17 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
6550
6729
  continue;
6551
6730
  }
6552
6731
  const appPort = app.appPort ?? await findFreePort();
6553
- const protocol = tls2 ? "https" : "http";
6554
- const portSuffix = tls2 && proxyPort === 443 || !tls2 && proxyPort === 80 ? "" : `:${proxyPort}`;
6555
- const url = `${protocol}://${app.name}.${tld}${portSuffix}`;
6732
+ const hostnames = buildHostnames(app.name, tlds);
6733
+ const urls = formatUrls(hostnames, proxyPort, tls2);
6734
+ const url = urls[0];
6556
6735
  appUrls.push({ label: app.label, url });
6557
- const hostname = parseHostname(app.name, tld);
6558
- store.addRoute(hostname, appPort, process.pid);
6559
- routes.push({ hostname });
6736
+ addRoutes(store, hostnames, appPort, process.pid);
6737
+ routes.push({ hostnames });
6560
6738
  const entry = {
6561
6739
  PORT: String(appPort),
6562
6740
  HOST: "127.0.0.1",
6563
- PORTLESS_URL: url
6741
+ PORTLESS_URL: url,
6742
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds)
6564
6743
  };
6565
6744
  if (tls2) {
6566
6745
  const caPath = path9.join(stateDir, "ca.pem");
@@ -6603,11 +6782,8 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
6603
6782
  killTree(turboChild, "SIGKILL");
6604
6783
  }
6605
6784
  }, SIGKILL_TIMEOUT_MS).unref();
6606
- for (const { hostname } of routes) {
6607
- try {
6608
- store.removeRoute(hostname, process.pid);
6609
- } catch {
6610
- }
6785
+ for (const { hostnames } of routes) {
6786
+ removeRoutes(store, hostnames, process.pid);
6611
6787
  }
6612
6788
  removeManifest();
6613
6789
  };
@@ -6621,7 +6797,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
6621
6797
  process.exit(exitCode);
6622
6798
  }
6623
6799
  }
6624
- async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, taskApps) {
6800
+ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tlds, proxiedApps, taskApps) {
6625
6801
  const children = [];
6626
6802
  const exitCodes = /* @__PURE__ */ new Map();
6627
6803
  const appUrls = [];
@@ -6632,7 +6808,7 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
6632
6808
  stateDir,
6633
6809
  proxyPort,
6634
6810
  tls2,
6635
- tld,
6811
+ tlds,
6636
6812
  exitCodes
6637
6813
  );
6638
6814
  children.push(child);
@@ -6667,11 +6843,8 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
6667
6843
  }
6668
6844
  }
6669
6845
  }, SIGKILL_TIMEOUT_MS).unref();
6670
- for (const { store, hostname } of routeEntries) {
6671
- try {
6672
- store.removeRoute(hostname, process.pid);
6673
- } catch {
6674
- }
6846
+ for (const { store, hostnames } of routeEntries) {
6847
+ removeRoutes(store, hostnames, process.pid);
6675
6848
  }
6676
6849
  };
6677
6850
  process.on("SIGINT", cleanup);
@@ -6730,7 +6903,7 @@ async function handleRunMode(args, globalScript) {
6730
6903
  }
6731
6904
  const worktree = detectWorktreePrefix();
6732
6905
  const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
6733
- const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
6906
+ const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6734
6907
  const store = new RouteStore(dir, {
6735
6908
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6736
6909
  });
@@ -6741,7 +6914,7 @@ async function handleRunMode(args, globalScript) {
6741
6914
  effectiveName,
6742
6915
  parsed.commandArgs,
6743
6916
  tls2,
6744
- tld,
6917
+ tlds,
6745
6918
  parsed.force,
6746
6919
  { nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
6747
6920
  parsed.appPort,
@@ -6767,7 +6940,7 @@ async function handleNamedMode(args) {
6767
6940
  }
6768
6941
  const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
6769
6942
  parseHostname(safeName, DEFAULT_TLD);
6770
- const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
6943
+ const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6771
6944
  const store = new RouteStore(dir, {
6772
6945
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6773
6946
  });
@@ -6778,7 +6951,7 @@ async function handleNamedMode(args) {
6778
6951
  safeName,
6779
6952
  parsed.commandArgs,
6780
6953
  tls2,
6781
- tld,
6954
+ tlds,
6782
6955
  parsed.force,
6783
6956
  void 0,
6784
6957
  parsed.appPort,