portless 0.15.0 → 0.15.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/dist/cli.js CHANGED
@@ -14,9 +14,10 @@ import {
14
14
  isErrnoException,
15
15
  isProcessAlive,
16
16
  parseHostname,
17
+ parseHostnames,
17
18
  shouldAutoSyncHosts,
18
19
  syncHostsFile
19
- } from "./chunk-PCBKLZK2.js";
20
+ } from "./chunk-SD2PIWJU.js";
20
21
 
21
22
  // src/colors.ts
22
23
  function supportsColor() {
@@ -501,15 +502,16 @@ async function generateHostCertAsync(stateDir, hostname) {
501
502
  fixOwnership(keyPath, certPath);
502
503
  return { certPath, keyPath };
503
504
  }
504
- function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost", caCert) {
505
+ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost", caCert) {
505
506
  const cache = /* @__PURE__ */ new Map();
506
507
  const pending = /* @__PURE__ */ new Map();
508
+ const configuredTlds = Array.isArray(tlds) ? tlds : [tlds];
507
509
  const defaultCtx = tls.createSecureContext({
508
510
  cert: caCert ? Buffer.concat([defaultCert, caCert]) : defaultCert,
509
511
  key: defaultKey
510
512
  });
511
513
  return (servername, cb) => {
512
- if (servername === tld) {
514
+ if (servername === "localhost" && configuredTlds.includes("localhost")) {
513
515
  cb(null, defaultCtx);
514
516
  return;
515
517
  }
@@ -1584,8 +1586,25 @@ function validateTld(tld) {
1584
1586
  }
1585
1587
  return null;
1586
1588
  }
1589
+ function parseTldList(value, source = "TLD") {
1590
+ const trimmed = value.trim();
1591
+ if (!trimmed) return [];
1592
+ const tlds = [];
1593
+ const seen = /* @__PURE__ */ new Set();
1594
+ for (const rawPart of trimmed.split(",")) {
1595
+ const tld = rawPart.trim().toLowerCase();
1596
+ const err = validateTld(tld);
1597
+ if (err) throw new Error(source === "TLD" ? err : `${source}: ${err}`);
1598
+ if (!seen.has(tld)) {
1599
+ seen.add(tld);
1600
+ tlds.push(tld);
1601
+ }
1602
+ }
1603
+ return tlds;
1604
+ }
1587
1605
  var TLD_FILE = "proxy.tld";
1588
- function readTldFromDir(dir) {
1606
+ var TLDS_FILE = "proxy.tlds";
1607
+ function readLegacyTldFromDir(dir) {
1589
1608
  try {
1590
1609
  const raw = fs3.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
1591
1610
  return raw || DEFAULT_TLD;
@@ -1593,23 +1612,43 @@ function readTldFromDir(dir) {
1593
1612
  return DEFAULT_TLD;
1594
1613
  }
1595
1614
  }
1596
- function writeTldFile(dir, tld) {
1597
- const filePath = path3.join(dir, TLD_FILE);
1598
- if (tld === DEFAULT_TLD) {
1615
+ function readTldsFromDir(dir) {
1616
+ try {
1617
+ const raw = fs3.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
1618
+ const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
1619
+ if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
1620
+ const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
1621
+ return tlds.length > 0 ? [...new Set(tlds)] : [DEFAULT_TLD];
1622
+ } catch {
1623
+ return [readLegacyTldFromDir(dir)];
1624
+ }
1625
+ }
1626
+ function writeTldsFile(dir, tlds) {
1627
+ const uniqueTlds = [...new Set(tlds)];
1628
+ const tldsPath = path3.join(dir, TLDS_FILE);
1629
+ const tldPath = path3.join(dir, TLD_FILE);
1630
+ if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
1599
1631
  try {
1600
- fs3.unlinkSync(filePath);
1632
+ fs3.unlinkSync(tldsPath);
1633
+ } catch {
1634
+ }
1635
+ try {
1636
+ fs3.unlinkSync(tldPath);
1601
1637
  } catch {
1602
1638
  }
1603
1639
  } else {
1604
- fs3.writeFileSync(filePath, tld, { mode: 420 });
1640
+ fs3.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
1641
+ fs3.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
1605
1642
  }
1606
1643
  }
1607
- function getDefaultTld() {
1644
+ function writeTldFile(dir, tld) {
1645
+ writeTldsFile(dir, [tld]);
1646
+ }
1647
+ function getDefaultTlds() {
1608
1648
  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;
1649
+ if (!val) return [DEFAULT_TLD];
1650
+ const tlds = parseTldList(val, "PORTLESS_TLD");
1651
+ return tlds.length > 0 ? tlds : [DEFAULT_TLD];
1613
1652
  }
1614
1653
  function isHttpsEnvDisabled() {
1615
1654
  const val = process.env.PORTLESS_HTTPS;
@@ -1628,14 +1667,17 @@ function readPersistedProxyState() {
1628
1667
  const port = readPortFromDir(dir);
1629
1668
  if (port !== null) {
1630
1669
  const tls2 = readTlsMarker(dir);
1631
- const tld = readTldFromDir(dir);
1670
+ const tlds = readTldsFromDir(dir);
1671
+ const tld = tlds[0] ?? DEFAULT_TLD;
1632
1672
  const lanIp = readLanMarker(dir);
1633
- return { port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local" };
1673
+ return { port, tls: tls2, tld, tlds, lanMode: lanIp !== null || tlds.includes("local") };
1634
1674
  }
1635
1675
  return null;
1636
1676
  }
1637
1677
  function buildProxyStartConfig(options) {
1638
- const effectiveTld = options.lanMode ? "local" : options.tld;
1678
+ const requestedTlds = options.tlds && options.tlds.length > 0 ? [...options.tlds] : [options.tld];
1679
+ const effectiveTlds = options.lanMode ? ["local"] : [...new Set(requestedTlds)];
1680
+ const effectiveTld = effectiveTlds[0] ?? DEFAULT_TLD;
1639
1681
  const args = [];
1640
1682
  if (options.foreground) {
1641
1683
  args.push("--foreground");
@@ -1661,8 +1703,10 @@ function buildProxyStartConfig(options) {
1661
1703
  args.push(INTERNAL_LAN_IP_FLAG, options.lanIp);
1662
1704
  }
1663
1705
  }
1664
- } else if (effectiveTld !== DEFAULT_TLD) {
1665
- args.push("--tld", effectiveTld);
1706
+ } else if (effectiveTlds.length > 1 || effectiveTld !== DEFAULT_TLD) {
1707
+ for (const tld of effectiveTlds) {
1708
+ args.push("--tld", tld);
1709
+ }
1666
1710
  }
1667
1711
  if (options.useWildcard) {
1668
1712
  args.push("--wildcard");
@@ -1670,7 +1714,7 @@ function buildProxyStartConfig(options) {
1670
1714
  if (options.skipTrust) {
1671
1715
  args.push("--skip-trust");
1672
1716
  }
1673
- return { effectiveTld, args };
1717
+ return { effectiveTld, effectiveTlds, args };
1674
1718
  }
1675
1719
  async function discoverState() {
1676
1720
  if (process.env.PORTLESS_STATE_DIR) {
@@ -1679,14 +1723,25 @@ async function discoverState() {
1679
1723
  const lanIp = readLanMarker(dir2);
1680
1724
  if (await isProxyRunning(port) || await isPortListening(port)) {
1681
1725
  const tls2 = readTlsMarker(dir2);
1682
- const tld = readTldFromDir(dir2);
1683
- return { dir: dir2, port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local", lanIp };
1726
+ const tlds3 = readTldsFromDir(dir2);
1727
+ const tld = tlds3[0] ?? DEFAULT_TLD;
1728
+ return {
1729
+ dir: dir2,
1730
+ port,
1731
+ tls: tls2,
1732
+ tld,
1733
+ tlds: tlds3,
1734
+ lanMode: lanIp !== null || tlds3.includes("local"),
1735
+ lanIp
1736
+ };
1684
1737
  }
1738
+ const tlds2 = readTldsFromDir(dir2);
1685
1739
  return {
1686
1740
  dir: dir2,
1687
1741
  port,
1688
1742
  tls: readTlsMarker(dir2),
1689
- tld: readTldFromDir(dir2),
1743
+ tld: tlds2[0] ?? DEFAULT_TLD,
1744
+ tlds: tlds2,
1690
1745
  lanMode: lanIp !== null,
1691
1746
  lanIp: null
1692
1747
  };
@@ -1695,14 +1750,16 @@ async function discoverState() {
1695
1750
  if (userPort !== null) {
1696
1751
  if (await isProxyRunning(userPort)) {
1697
1752
  const tls2 = readTlsMarker(USER_STATE_DIR);
1698
- const tld = readTldFromDir(USER_STATE_DIR);
1753
+ const tlds2 = readTldsFromDir(USER_STATE_DIR);
1754
+ const tld = tlds2[0] ?? DEFAULT_TLD;
1699
1755
  const lanIp = readLanMarker(USER_STATE_DIR);
1700
1756
  return {
1701
1757
  dir: USER_STATE_DIR,
1702
1758
  port: userPort,
1703
1759
  tls: tls2,
1704
1760
  tld,
1705
- lanMode: lanIp !== null || tld === "local",
1761
+ tlds: tlds2,
1762
+ lanMode: lanIp !== null || tlds2.includes("local"),
1706
1763
  lanIp
1707
1764
  };
1708
1765
  }
@@ -1711,14 +1768,16 @@ async function discoverState() {
1711
1768
  if (legacyPort !== null) {
1712
1769
  if (await isProxyRunning(legacyPort)) {
1713
1770
  const tls2 = readTlsMarker(LEGACY_SYSTEM_STATE_DIR);
1714
- const tld = readTldFromDir(LEGACY_SYSTEM_STATE_DIR);
1771
+ const tlds2 = readTldsFromDir(LEGACY_SYSTEM_STATE_DIR);
1772
+ const tld = tlds2[0] ?? DEFAULT_TLD;
1715
1773
  const lanIp = readLanMarker(LEGACY_SYSTEM_STATE_DIR);
1716
1774
  return {
1717
1775
  dir: LEGACY_SYSTEM_STATE_DIR,
1718
1776
  port: legacyPort,
1719
1777
  tls: tls2,
1720
1778
  tld,
1721
- lanMode: lanIp !== null || tld === "local",
1779
+ tlds: tlds2,
1780
+ lanMode: lanIp !== null || tlds2.includes("local"),
1722
1781
  lanIp
1723
1782
  };
1724
1783
  }
@@ -1730,17 +1789,28 @@ async function discoverState() {
1730
1789
  const dir2 = resolveStateDir(port);
1731
1790
  const markerTls = readTlsMarker(dir2);
1732
1791
  const tls2 = markerTls || port === getProtocolPort(true);
1733
- const tld = readTldFromDir(dir2);
1792
+ const tlds2 = readTldsFromDir(dir2);
1793
+ const tld = tlds2[0] ?? DEFAULT_TLD;
1734
1794
  const lanIp = readLanMarker(dir2);
1735
- return { dir: dir2, port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local", lanIp };
1795
+ return {
1796
+ dir: dir2,
1797
+ port,
1798
+ tls: tls2,
1799
+ tld,
1800
+ tlds: tlds2,
1801
+ lanMode: lanIp !== null || tlds2.includes("local"),
1802
+ lanIp
1803
+ };
1736
1804
  }
1737
1805
  }
1738
1806
  const dir = resolveStateDir(configuredPort);
1807
+ const tlds = readTldsFromDir(dir);
1739
1808
  return {
1740
1809
  dir,
1741
1810
  port: configuredPort,
1742
1811
  tls: readTlsMarker(dir),
1743
- tld: readTldFromDir(dir),
1812
+ tld: tlds[0] ?? DEFAULT_TLD,
1813
+ tlds,
1744
1814
  lanMode: readLanMarker(dir) !== null,
1745
1815
  lanIp: null
1746
1816
  };
@@ -2025,6 +2095,7 @@ var PORTLESS_STATE_FILES = [
2025
2095
  "proxy.tls",
2026
2096
  "proxy.custom-cert",
2027
2097
  "proxy.tld",
2098
+ "proxy.tlds",
2028
2099
  "proxy.lan",
2029
2100
  "ca-key.pem",
2030
2101
  "ca.pem",
@@ -2797,6 +2868,15 @@ var SYSTEMD_SERVICE = "portless.service";
2797
2868
  var WINDOWS_TASK_NAME = "Portless Proxy";
2798
2869
  var INTERNAL_ELEVATED_ENV = "PORTLESS_INTERNAL_SERVICE_ELEVATED";
2799
2870
  var SERVICE_ENV_KEYS = /* @__PURE__ */ new Set(["PORTLESS_SYNC_HOSTS"]);
2871
+ function normalizeTlds(tlds) {
2872
+ return [...new Set(tlds.length > 0 ? tlds : [DEFAULT_TLD])];
2873
+ }
2874
+ function primaryTld(tlds) {
2875
+ return tlds[0] ?? DEFAULT_TLD;
2876
+ }
2877
+ function formatTldList(tlds) {
2878
+ return tlds.map((tld) => `.${tld}`).join(", ");
2879
+ }
2800
2880
  var DEFAULT_SERVICE_CONFIG = {
2801
2881
  proxyPort: DEFAULT_SERVICE_PORT,
2802
2882
  useHttps: true,
@@ -2806,6 +2886,7 @@ var DEFAULT_SERVICE_CONFIG = {
2806
2886
  lanIp: null,
2807
2887
  lanIpExplicit: false,
2808
2888
  tld: DEFAULT_TLD,
2889
+ tlds: [DEFAULT_TLD],
2809
2890
  useWildcard: false,
2810
2891
  extraEnv: {}
2811
2892
  };
@@ -2892,10 +2973,8 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2892
2973
  config.lanIpExplicit = true;
2893
2974
  }
2894
2975
  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;
2976
+ config.tlds = normalizeTlds(parseTldList(env.PORTLESS_TLD, "PORTLESS_TLD"));
2977
+ config.tld = primaryTld(config.tlds);
2899
2978
  }
2900
2979
  const envWildcard = parseBooleanEnv(env.PORTLESS_WILDCARD);
2901
2980
  if (envWildcard !== null) {
@@ -2907,6 +2986,7 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2907
2986
  config.proxyPort = getProtocolPort(config.useHttps);
2908
2987
  }
2909
2988
  const tokens = args[0] === "service" ? args.slice(2) : args;
2989
+ let tldFlagSeen = false;
2910
2990
  for (let i = 0; i < tokens.length; i += 1) {
2911
2991
  const token = tokens[i];
2912
2992
  switch (token) {
@@ -2931,10 +3011,10 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2931
3011
  i += 1;
2932
3012
  break;
2933
3013
  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;
3014
+ const tlds = parseTldList(getFlagValue(tokens, i, token));
3015
+ config.tlds = normalizeTlds([...tldFlagSeen ? config.tlds : [], ...tlds]);
3016
+ config.tld = primaryTld(config.tlds);
3017
+ tldFlagSeen = true;
2938
3018
  i += 1;
2939
3019
  break;
2940
3020
  }
@@ -2974,6 +3054,9 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
2974
3054
  if (!config.lanMode) {
2975
3055
  config.lanIp = null;
2976
3056
  config.lanIpExplicit = false;
3057
+ } else {
3058
+ config.tlds = ["local"];
3059
+ config.tld = "local";
2977
3060
  }
2978
3061
  return config;
2979
3062
  }
@@ -3019,6 +3102,7 @@ function buildProxyCommand(entryScript, serviceConfig) {
3019
3102
  lanIp: serviceConfig.lanIp,
3020
3103
  lanIpExplicit: serviceConfig.lanIpExplicit,
3021
3104
  tld: serviceConfig.tld,
3105
+ tlds: serviceConfig.tlds,
3022
3106
  useWildcard: serviceConfig.useWildcard,
3023
3107
  foreground: true,
3024
3108
  includePort: true,
@@ -3041,8 +3125,8 @@ function buildServiceEnv(ctx) {
3041
3125
  }
3042
3126
  if (ctx.config.lanMode) {
3043
3127
  env.PORTLESS_TLD = "local";
3044
- } else if (ctx.config.tld !== DEFAULT_TLD) {
3045
- env.PORTLESS_TLD = ctx.config.tld;
3128
+ } else if (ctx.config.tlds.length > 1 || ctx.config.tld !== DEFAULT_TLD) {
3129
+ env.PORTLESS_TLD = ctx.config.tlds.join(",");
3046
3130
  }
3047
3131
  if (ctx.platform === "win32") {
3048
3132
  env.USERPROFILE = ctx.user.home;
@@ -3127,6 +3211,10 @@ function buildServiceSpec(options) {
3127
3211
  ...options.installConfig,
3128
3212
  extraEnv: options.installConfig?.extraEnv ?? {}
3129
3213
  };
3214
+ installConfig.tlds = installConfig.lanMode ? ["local"] : normalizeTlds(
3215
+ options.installConfig?.tlds ?? (options.installConfig?.tld ? [options.installConfig.tld] : installConfig.tlds)
3216
+ );
3217
+ installConfig.tld = primaryTld(installConfig.tlds);
3130
3218
  const stateDir = options.stateDir || installConfig.stateDir || defaultStateDir(options.platform, options.userHome);
3131
3219
  const normalizedConfig = {
3132
3220
  ...installConfig,
@@ -3617,7 +3705,7 @@ async function printServiceStatus(entryScript, runner) {
3617
3705
  ` Proxy on ${config.proxyPort}: ${status.proxyRunning ? "responding" : "not responding"}`
3618
3706
  );
3619
3707
  console.log(` HTTPS: ${config.useHttps ? "yes" : "no"}`);
3620
- console.log(` TLD: ${config.lanMode ? "local" : config.tld}`);
3708
+ console.log(` TLDs: ${config.lanMode ? ".local" : formatTldList(config.tlds)}`);
3621
3709
  console.log(` LAN mode: ${config.lanMode ? "yes" : "no"}`);
3622
3710
  if (config.lanIpExplicit && config.lanIp) {
3623
3711
  console.log(` LAN IP: ${config.lanIp}`);
@@ -3645,7 +3733,7 @@ ${colors_default.bold("Install options:")}
3645
3733
  --https Enable HTTPS
3646
3734
  --lan Enable LAN mode
3647
3735
  --ip <address> Pin a specific LAN IP
3648
- --tld <tld> Use a custom TLD outside LAN mode
3736
+ --tld <tld> Use a custom TLD outside LAN mode, repeatable
3649
3737
  --wildcard Allow subdomain fallback
3650
3738
  --cert <path> Use a custom TLS certificate
3651
3739
  --key <path> Use a custom TLS private key
@@ -3695,7 +3783,17 @@ var DEBOUNCE_MS = 100;
3695
3783
  var POLL_INTERVAL_MS = 3e3;
3696
3784
  var EXIT_TIMEOUT_MS = 2e3;
3697
3785
  var SUDO_SPAWN_TIMEOUT_MS = 3e4;
3698
- function defaultProxyConfig(tld, useHttps, lanMode) {
3786
+ function normalizeTlds2(tlds) {
3787
+ return [...new Set(tlds.length > 0 ? tlds : [DEFAULT_TLD])];
3788
+ }
3789
+ function primaryTld2(tlds) {
3790
+ return tlds[0] ?? DEFAULT_TLD;
3791
+ }
3792
+ function formatTldList2(tlds) {
3793
+ return tlds.map((tld) => `.${tld}`).join(", ");
3794
+ }
3795
+ function defaultProxyConfig(tlds, useHttps, lanMode) {
3796
+ const effectiveTlds = lanMode ? ["local"] : normalizeTlds2(tlds);
3699
3797
  return {
3700
3798
  useHttps,
3701
3799
  customCertPath: null,
@@ -3703,13 +3801,14 @@ function defaultProxyConfig(tld, useHttps, lanMode) {
3703
3801
  lanMode,
3704
3802
  lanIp: null,
3705
3803
  lanIpExplicit: false,
3706
- tld: lanMode ? "local" : tld,
3804
+ tld: primaryTld2(effectiveTlds),
3805
+ tlds: effectiveTlds,
3707
3806
  useWildcard: false
3708
3807
  };
3709
3808
  }
3710
3809
  function resolveProxyConfig(options) {
3711
3810
  const config = defaultProxyConfig(
3712
- options.defaultTld,
3811
+ options.defaultTlds,
3713
3812
  options.useHttps,
3714
3813
  options.explicit.lanMode ? options.lanMode : options.persistedLanMode
3715
3814
  );
@@ -3730,8 +3829,9 @@ function resolveProxyConfig(options) {
3730
3829
  if (!options.lanMode) {
3731
3830
  config.lanIp = null;
3732
3831
  config.lanIpExplicit = false;
3733
- if (!options.explicit.tld) {
3734
- config.tld = options.defaultTld;
3832
+ if (!options.explicit.tlds) {
3833
+ config.tlds = normalizeTlds2(options.defaultTlds);
3834
+ config.tld = primaryTld2(config.tlds);
3735
3835
  }
3736
3836
  }
3737
3837
  }
@@ -3740,8 +3840,9 @@ function resolveProxyConfig(options) {
3740
3840
  config.lanIp = options.lanIp;
3741
3841
  config.lanIpExplicit = true;
3742
3842
  }
3743
- if (options.explicit.tld) {
3744
- config.tld = options.tld;
3843
+ if (options.explicit.tlds) {
3844
+ config.tlds = normalizeTlds2(options.tlds);
3845
+ config.tld = primaryTld2(config.tlds);
3745
3846
  }
3746
3847
  if (options.explicit.useWildcard) {
3747
3848
  config.useWildcard = options.useWildcard;
@@ -3751,6 +3852,7 @@ function resolveProxyConfig(options) {
3751
3852
  config.lanIpExplicit = false;
3752
3853
  }
3753
3854
  if (config.lanMode) {
3855
+ config.tlds = ["local"];
3754
3856
  config.tld = "local";
3755
3857
  if (!config.lanIpExplicit) {
3756
3858
  config.lanIp = null;
@@ -3764,15 +3866,17 @@ function resolveProxyConfig(options) {
3764
3866
  }
3765
3867
  function readCurrentProxyConfig(dir) {
3766
3868
  const lanIp = readLanMarker(dir);
3767
- const tld = readTldFromDir(dir);
3869
+ const tlds = readTldsFromDir(dir);
3870
+ const tld = primaryTld2(tlds);
3768
3871
  return {
3769
3872
  useHttps: readTlsMarker(dir),
3770
3873
  customCertPath: null,
3771
3874
  customKeyPath: null,
3772
- lanMode: lanIp !== null || tld === "local",
3875
+ lanMode: lanIp !== null || tlds.includes("local"),
3773
3876
  lanIp,
3774
3877
  lanIpExplicit: false,
3775
3878
  tld,
3879
+ tlds,
3776
3880
  useWildcard: false
3777
3881
  };
3778
3882
  }
@@ -3793,9 +3897,9 @@ function getProxyConfigMismatchMessages(desiredConfig, actualConfig, explicit) {
3793
3897
  desiredConfig.useHttps ? "requested HTTPS, but the running proxy is using HTTP" : "requested HTTP, but the running proxy is using HTTPS"
3794
3898
  );
3795
3899
  }
3796
- if (explicit.tld && desiredConfig.tld !== actualConfig.tld) {
3900
+ if (explicit.tlds && (desiredConfig.tlds.length !== actualConfig.tlds.length || desiredConfig.tlds.some((tld, index) => tld !== actualConfig.tlds[index]))) {
3797
3901
  messages.push(
3798
- `requested .${desiredConfig.tld}, but the running proxy is using .${actualConfig.tld}`
3902
+ `requested ${formatTldList2(desiredConfig.tlds)}, but the running proxy is using ${formatTldList2(actualConfig.tlds)}`
3799
3903
  );
3800
3904
  }
3801
3905
  return messages;
@@ -3810,6 +3914,7 @@ function formatProxyStartCommand(proxyPort, config) {
3810
3914
  lanIp: config.lanIpExplicit ? config.lanIp : null,
3811
3915
  lanIpExplicit: config.lanIpExplicit,
3812
3916
  tld: config.tld,
3917
+ tlds: config.tlds,
3813
3918
  useWildcard: config.useWildcard,
3814
3919
  includePort: proxyPort !== getDefaultPort(config.useHttps),
3815
3920
  proxyPort
@@ -3903,7 +4008,46 @@ function formatProcessExitSuffix(code, signal) {
3903
4008
  if (code !== null) return ` (exit ${code})`;
3904
4009
  return "";
3905
4010
  }
3906
- function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict, customCert = false) {
4011
+ function buildHostnames(name, tlds) {
4012
+ return parseHostnames(name, normalizeTlds2(tlds));
4013
+ }
4014
+ function formatUrls(hostnames, proxyPort, tls2) {
4015
+ return hostnames.map((hostname) => formatUrl(hostname, proxyPort, tls2));
4016
+ }
4017
+ function formatViteAllowedHosts(tlds) {
4018
+ return tlds.map((configuredTld) => `.${configuredTld}`).join(",");
4019
+ }
4020
+ function addRoutes(store, hostnames, port, pid, force = false) {
4021
+ const registered = [];
4022
+ const killedPids = [];
4023
+ try {
4024
+ for (const hostname of hostnames) {
4025
+ const killedPid = store.addRoute(hostname, port, pid, force);
4026
+ registered.push(hostname);
4027
+ if (killedPid !== void 0) {
4028
+ killedPids.push(killedPid);
4029
+ }
4030
+ }
4031
+ } catch (err) {
4032
+ for (const hostname of registered) {
4033
+ try {
4034
+ store.removeRoute(hostname, pid);
4035
+ } catch {
4036
+ }
4037
+ }
4038
+ throw err;
4039
+ }
4040
+ return [...new Set(killedPids)];
4041
+ }
4042
+ function removeRoutes(store, hostnames, ownerPid) {
4043
+ for (const hostname of hostnames) {
4044
+ try {
4045
+ store.removeRoute(hostname, ownerPid);
4046
+ } catch {
4047
+ }
4048
+ }
4049
+ }
4050
+ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict, customCert = false) {
3907
4051
  store.ensureDir();
3908
4052
  const isTls = !!tlsOptions;
3909
4053
  const mdnsSupport = isMdnsSupported();
@@ -3995,6 +4139,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict, cust
3995
4139
  getRoutes: () => cachedRoutes,
3996
4140
  proxyPort,
3997
4141
  tld,
4142
+ tlds,
3998
4143
  strict,
3999
4144
  onError: (msg) => console.error(colors_default.red(msg)),
4000
4145
  tls: tlsOptions
@@ -4033,11 +4178,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict, cust
4033
4178
  fs9.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
4034
4179
  writeTlsMarker(store.dir, isTls);
4035
4180
  writeCustomCertMarker(store.dir, isTls && customCert);
4036
- writeTldFile(store.dir, tld);
4181
+ writeTldsFile(store.dir, tlds);
4037
4182
  writeLanMarker(store.dir, activeLanIp);
4038
4183
  fixOwnership(store.dir, store.pidPath, store.portFilePath);
4039
4184
  const proto = isTls ? "HTTPS/2" : "HTTP";
4040
- const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
4185
+ const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
4041
4186
  const modeLabel = strict === false ? " (wildcard)" : "";
4042
4187
  console.log(
4043
4188
  colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
@@ -4255,28 +4400,29 @@ function listRoutes(store, proxyPort, tls2) {
4255
4400
  console.log();
4256
4401
  }
4257
4402
  function resolveProxyDesiredState(lanMode) {
4258
- const envTld = getDefaultTld();
4403
+ const envTlds = getDefaultTlds();
4404
+ const envTld = primaryTld2(envTlds);
4259
4405
  const explicit = {
4260
4406
  useHttps: process.env.PORTLESS_HTTPS !== void 0,
4261
4407
  customCert: false,
4262
4408
  lanMode: process.env.PORTLESS_LAN !== void 0,
4263
4409
  lanIp: process.env.PORTLESS_LAN_IP !== void 0,
4264
- tld: process.env.PORTLESS_TLD !== void 0,
4410
+ tlds: process.env.PORTLESS_TLD !== void 0,
4265
4411
  useWildcard: process.env.PORTLESS_WILDCARD !== void 0
4266
4412
  };
4267
4413
  const desiredConfig = resolveProxyConfig({
4268
4414
  persistedLanMode: lanMode,
4269
4415
  explicit,
4270
- defaultTld: envTld,
4416
+ defaultTlds: envTlds,
4271
4417
  useHttps: !isHttpsEnvDisabled(),
4272
4418
  customCertPath: null,
4273
4419
  customKeyPath: null,
4274
4420
  lanMode: isLanEnvEnabled(),
4275
4421
  lanIp: process.env.PORTLESS_LAN_IP || null,
4276
- tld: envTld,
4422
+ tlds: envTlds,
4277
4423
  useWildcard: isWildcardEnvEnabled()
4278
4424
  });
4279
- return { explicit, desiredConfig, envTld };
4425
+ return { explicit, desiredConfig, envTld, envTlds };
4280
4426
  }
4281
4427
  async function ensureProxyRunning(proxyPort, tls2, desired) {
4282
4428
  const { explicit, desiredConfig } = desired;
@@ -4292,8 +4438,9 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4292
4438
  if (!explicit.useHttps && persisted.tls !== desiredConfig.useHttps) {
4293
4439
  startConfig.useHttps = persisted.tls;
4294
4440
  }
4295
- if (!explicit.tld && persisted.tld !== desiredConfig.tld) {
4296
- startConfig.tld = persisted.tld;
4441
+ if (!explicit.tlds && (persisted.tlds.length !== desiredConfig.tlds.length || persisted.tlds.some((tld, index) => tld !== desiredConfig.tlds[index]))) {
4442
+ startConfig.tlds = persisted.tlds;
4443
+ startConfig.tld = primaryTld2(persisted.tlds);
4297
4444
  }
4298
4445
  if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
4299
4446
  startConfig.lanMode = persisted.lanMode;
@@ -4329,6 +4476,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4329
4476
  lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
4330
4477
  lanIpExplicit: startConfig.lanIpExplicit,
4331
4478
  tld: startConfig.tld,
4479
+ tlds: startConfig.tlds,
4332
4480
  useWildcard: startConfig.useWildcard,
4333
4481
  includePort: startPort !== void 0,
4334
4482
  proxyPort: startPort
@@ -4363,7 +4511,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4363
4511
  }
4364
4512
  return { started: true, state: discovered };
4365
4513
  }
4366
- async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort, lanMode = false, lanIp) {
4514
+ async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tlds, force, autoInfo, desiredPort, lanMode = false, lanIp) {
4367
4515
  let store = initialStore;
4368
4516
  console.log(chalk.blue.bold(`
4369
4517
  portless
@@ -4410,12 +4558,12 @@ portless
4410
4558
  console.error(colors_default.red(`Error: ${err.message}`));
4411
4559
  process.exit(1);
4412
4560
  }
4413
- parseHostname(name, tld);
4561
+ buildHostnames(name, tlds);
4414
4562
  const ensureResult = await ensureProxyRunning(proxyPort, tls2, desired);
4415
4563
  if (ensureResult.started) {
4416
4564
  proxyPort = ensureResult.state.port;
4417
4565
  stateDir = ensureResult.state.dir;
4418
- tld = ensureResult.state.tld;
4566
+ tlds = ensureResult.state.tlds;
4419
4567
  tls2 = ensureResult.state.tls;
4420
4568
  lanMode = ensureResult.state.lanMode;
4421
4569
  lanIp = ensureResult.state.lanIp;
@@ -4437,20 +4585,22 @@ portless
4437
4585
  }
4438
4586
  lanMode = runningConfig.lanMode;
4439
4587
  lanIp = runningConfig.lanIp;
4588
+ tlds = runningConfig.tlds;
4440
4589
  console.log(chalk.gray("-- Proxy is running"));
4441
4590
  }
4442
- const hostname = parseHostname(name, tld);
4443
- if (desired.envTld !== DEFAULT_TLD && desired.envTld !== tld) {
4591
+ const hostnames = buildHostnames(name, tlds);
4592
+ const hostname = hostnames[0];
4593
+ if (desired.explicit.tlds && (desired.envTlds.some((envConfiguredTld, index) => envConfiguredTld !== tlds[index]) || desired.envTlds.length !== tlds.length)) {
4444
4594
  console.warn(
4445
4595
  chalk.yellow(
4446
- `Warning: PORTLESS_TLD=${desired.envTld} but the running proxy uses .${tld}. Using .${tld}.`
4596
+ `Warning: PORTLESS_TLD=${desired.envTlds.join(",")} but the running proxy uses ${formatTldList2(tlds)}.`
4447
4597
  )
4448
4598
  );
4449
4599
  }
4450
4600
  if (lanIp) {
4451
- console.log(chalk.gray(`-- ${hostname} (LAN: ${lanIp})`));
4601
+ console.log(chalk.gray(`-- ${hostnames.join(", ")} (LAN: ${lanIp})`));
4452
4602
  } else {
4453
- console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
4603
+ console.log(chalk.gray(`-- ${hostnames.join(", ")} (auto-resolves to 127.0.0.1)`));
4454
4604
  }
4455
4605
  if (autoInfo) {
4456
4606
  const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
@@ -4465,9 +4615,9 @@ portless
4465
4615
  } else {
4466
4616
  console.log(colors_default.green(`-- Using port ${port}`));
4467
4617
  }
4468
- let killedPid;
4618
+ let killedPids = [];
4469
4619
  try {
4470
- killedPid = store.addRoute(hostname, port, process.pid, force);
4620
+ killedPids = addRoutes(store, hostnames, port, process.pid, force);
4471
4621
  } catch (err) {
4472
4622
  if (err instanceof RouteConflictError) {
4473
4623
  console.error(colors_default.red(`Error: ${err.message}`));
@@ -4475,13 +4625,20 @@ portless
4475
4625
  }
4476
4626
  throw err;
4477
4627
  }
4478
- if (killedPid !== void 0) {
4479
- console.log(colors_default.yellow(`Killed existing process (PID ${killedPid})`));
4628
+ if (killedPids.length > 0) {
4629
+ console.log(colors_default.yellow(`Killed existing process(es): ${killedPids.join(", ")}`));
4480
4630
  }
4481
4631
  const finalUrl = formatUrl(hostname, proxyPort, tls2);
4632
+ const allUrls = formatUrls(hostnames, proxyPort, tls2);
4482
4633
  console.log(chalk.cyan.bold(`
4483
4634
  -> ${finalUrl}
4484
4635
  `));
4636
+ for (const extraUrl of allUrls.slice(1)) {
4637
+ console.log(chalk.cyan(` also -> ${extraUrl}`));
4638
+ }
4639
+ if (allUrls.length > 1) {
4640
+ console.log("");
4641
+ }
4485
4642
  if (lanIp) {
4486
4643
  console.log(chalk.green(` LAN -> ${finalUrl}`));
4487
4644
  console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
@@ -4594,7 +4751,7 @@ portless
4594
4751
  } catch {
4595
4752
  }
4596
4753
  try {
4597
- store.removeRoute(hostname, process.pid);
4754
+ removeRoutes(store, hostnames, process.pid);
4598
4755
  } catch {
4599
4756
  }
4600
4757
  process.exit(1);
@@ -4628,7 +4785,7 @@ portless
4628
4785
  PORT: port.toString(),
4629
4786
  ...hostBind ? { HOST: hostBind } : {},
4630
4787
  PORTLESS_URL: finalUrl,
4631
- __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`,
4788
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds),
4632
4789
  // Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
4633
4790
  // baked-in pinging, making this env var ineffective. Expo handles its
4634
4791
  // own LAN discovery natively.
@@ -4648,7 +4805,7 @@ portless
4648
4805
  } catch {
4649
4806
  }
4650
4807
  try {
4651
- store.removeRoute(hostname, process.pid);
4808
+ removeRoutes(store, hostnames, process.pid);
4652
4809
  } catch {
4653
4810
  }
4654
4811
  }
@@ -4941,7 +5098,7 @@ ${colors_default.bold("Options:")}
4941
5098
  --cert <path> Use a custom TLS certificate
4942
5099
  --key <path> Use a custom TLS private key
4943
5100
  --foreground Run proxy in foreground (for debugging)
4944
- --tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
5101
+ --tld <tld> Use a custom TLD instead of .localhost; repeat for more
4945
5102
  --wildcard Allow unregistered subdomains to fall back to parent route
4946
5103
  --state-dir <path> Use a custom state directory with service install
4947
5104
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
@@ -4958,7 +5115,7 @@ ${colors_default.bold("Environment variables:")}
4958
5115
  PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
4959
5116
  PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
4960
5117
  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)
5118
+ PORTLESS_TLD=<tld>[,<tld>] Use one or more TLDs (e.g. localhost,test)
4962
5119
  PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
4963
5120
  PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
4964
5121
  PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
@@ -4970,7 +5127,7 @@ ${colors_default.bold("Environment variables:")}
4970
5127
  ${colors_default.bold("Child process environment:")}
4971
5128
  PORT Ephemeral port the child should listen on
4972
5129
  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)
5130
+ PORTLESS_URL Primary public URL of the app
4974
5131
  PORTLESS_LAN Set to 1 when proxy is in LAN mode
4975
5132
  PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
4976
5133
  PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
@@ -4996,7 +5153,7 @@ ${colors_default.bold("Reserved names:")}
4996
5153
  process.exit(0);
4997
5154
  }
4998
5155
  function printVersion() {
4999
- console.log("0.15.0");
5156
+ console.log("0.15.1");
5000
5157
  process.exit(0);
5001
5158
  }
5002
5159
  async function handleTrust() {
@@ -5271,8 +5428,8 @@ ${colors_default.bold("Examples:")}
5271
5428
  const name = positional[0];
5272
5429
  const worktree = skipWorktree ? null : detectWorktreePrefix();
5273
5430
  const effectiveName = worktree ? `${worktree.prefix}.${name}` : name;
5274
- const { port, tls: tls2, tld } = await discoverState();
5275
- const hostname = parseHostname(effectiveName, tld);
5431
+ const { port, tls: tls2, tlds } = await discoverState();
5432
+ const hostname = buildHostnames(effectiveName, tlds)[0];
5276
5433
  const url = formatUrl(hostname, port, tls2);
5277
5434
  process.stdout.write(url + "\n");
5278
5435
  }
@@ -5293,7 +5450,7 @@ ${colors_default.bold("Examples:")}
5293
5450
  `);
5294
5451
  process.exit(0);
5295
5452
  }
5296
- const { dir, tld } = await discoverState();
5453
+ const { dir, tlds } = await discoverState();
5297
5454
  const store = new RouteStore(dir, {
5298
5455
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
5299
5456
  });
@@ -5304,15 +5461,15 @@ ${colors_default.bold("Examples:")}
5304
5461
  console.error(colors_default.cyan(" portless alias --remove <name>"));
5305
5462
  process.exit(1);
5306
5463
  }
5307
- const hostname2 = parseHostname(aliasName2, tld);
5464
+ const hostnames2 = buildHostnames(aliasName2, tlds);
5308
5465
  const routes = store.loadRoutes();
5309
- const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
5466
+ const existing = routes.find((r) => hostnames2.includes(r.hostname) && r.pid === 0);
5310
5467
  if (!existing) {
5311
- console.error(colors_default.red(`Error: No alias found for "${hostname2}".`));
5468
+ console.error(colors_default.red(`Error: No alias found for "${hostnames2.join(", ")}".`));
5312
5469
  process.exit(1);
5313
5470
  }
5314
- store.removeRoute(hostname2);
5315
- console.log(colors_default.green(`Removed alias: ${hostname2}`));
5471
+ removeRoutes(store, hostnames2);
5472
+ console.log(colors_default.green(`Removed alias: ${hostnames2.join(", ")}`));
5316
5473
  return;
5317
5474
  }
5318
5475
  const aliasName = args[1];
@@ -5326,15 +5483,15 @@ ${colors_default.bold("Examples:")}
5326
5483
  console.error(colors_default.cyan(" portless alias my-postgres 5432"));
5327
5484
  process.exit(1);
5328
5485
  }
5329
- const hostname = parseHostname(aliasName, tld);
5486
+ const hostnames = buildHostnames(aliasName, tlds);
5330
5487
  const port = parseInt(aliasPort, 10);
5331
5488
  if (isNaN(port) || port < 1 || port > 65535) {
5332
5489
  console.error(colors_default.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
5333
5490
  process.exit(1);
5334
5491
  }
5335
5492
  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}`));
5493
+ addRoutes(store, hostnames, port, 0, force);
5494
+ console.log(colors_default.green(`Alias registered: ${hostnames.join(", ")} -> 127.0.0.1:${port}`));
5338
5495
  }
5339
5496
  async function handleHosts(args) {
5340
5497
  if (args[1] === "--help" || args[1] === "-h") {
@@ -5524,6 +5681,7 @@ ${colors_default.bold("Options:")}
5524
5681
  port: getDefaultPort(!isHttpsEnvDisabled()),
5525
5682
  tls: !isHttpsEnvDisabled(),
5526
5683
  tld: DEFAULT_TLD,
5684
+ tlds: [DEFAULT_TLD],
5527
5685
  lanMode: isLanEnvEnabled(),
5528
5686
  lanIp: null
5529
5687
  };
@@ -5545,12 +5703,14 @@ ${colors_default.bold("Options:")}
5545
5703
  const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
5546
5704
  const stateExists = fs9.existsSync(state.dir);
5547
5705
  console.log(colors_default.blue.bold("\nportless doctor\n"));
5548
- console.log(`Version: ${"0.15.0"}`);
5706
+ console.log(`Version: ${"0.15.1"}`);
5549
5707
  console.log(`Node.js: ${process.versions.node}`);
5550
5708
  console.log(`Platform: ${process.platform} ${process.arch}`);
5551
5709
  console.log(`State dir: ${state.dir}`);
5552
5710
  console.log(`Proxy target: ${formatUrl("127.0.0.1", proxyPort, proxyTls)}`);
5553
- console.log(`Mode: ${proxyTls ? "HTTPS" : "HTTP"}, .${state.tld}${state.lanMode ? ", LAN" : ""}`);
5711
+ console.log(
5712
+ `Mode: ${proxyTls ? "HTTPS" : "HTTP"}, ${formatTldList2(state.tlds)}${state.lanMode ? ", LAN" : ""}`
5713
+ );
5554
5714
  console.log("");
5555
5715
  const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
5556
5716
  if (nodeMajor >= 24) {
@@ -5822,6 +5982,7 @@ ${colors_default.bold("Usage:")}
5822
5982
  ${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
5823
5983
  ${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
5824
5984
  ${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
5985
+ ${colors_default.cyan("portless proxy start --tld localhost --tld test")} Serve both TLDs
5825
5986
  ${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
5826
5987
  ${colors_default.cyan("portless proxy stop")} Stop the proxy
5827
5988
 
@@ -5883,34 +6044,41 @@ ${colors_default.bold("LAN mode (--lan):")}
5883
6044
  }
5884
6045
  hasExplicitPort = true;
5885
6046
  }
5886
- let tld;
6047
+ let tlds;
5887
6048
  try {
5888
- tld = getDefaultTld();
6049
+ tlds = getDefaultTlds();
5889
6050
  } catch (err) {
5890
6051
  console.error(colors_default.red(`Error: ${err.message}`));
5891
6052
  process.exit(1);
5892
6053
  }
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);
6054
+ const tldFlagValues = [];
6055
+ for (let i = 0; i < args.length; i += 1) {
6056
+ if (args[i] === "--tld") {
6057
+ const tldValue = args[i + 1];
6058
+ if (!tldValue || tldValue.startsWith("-")) {
6059
+ console.error(colors_default.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
6060
+ process.exit(1);
6061
+ }
6062
+ tldFlagValues.push(tldValue);
6063
+ i += 1;
5899
6064
  }
5900
- tld = tldValue.trim().toLowerCase();
5901
- const tldErr = validateTld(tld);
5902
- if (tldErr) {
5903
- console.error(colors_default.red(`Error: ${tldErr}`));
6065
+ }
6066
+ if (tldFlagValues.length > 0) {
6067
+ try {
6068
+ tlds = normalizeTlds2(tldFlagValues.flatMap((value) => parseTldList(value)));
6069
+ } catch (err) {
6070
+ console.error(colors_default.red(`Error: ${err.message}`));
5904
6071
  process.exit(1);
5905
6072
  }
5906
6073
  }
6074
+ let tld = primaryTld2(tlds);
5907
6075
  const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
5908
6076
  const explicit = {
5909
6077
  useHttps: hasHttpsFlag || hasNoTls || customCertPath !== null || customKeyPath !== null || process.env.PORTLESS_HTTPS !== void 0,
5910
6078
  customCert: customCertPath !== null || customKeyPath !== null,
5911
6079
  lanMode: process.env.PORTLESS_LAN !== void 0,
5912
6080
  lanIp: process.env.PORTLESS_LAN_IP !== void 0,
5913
- tld: tldIdx !== -1 || process.env.PORTLESS_TLD !== void 0,
6081
+ tlds: tldFlagValues.length > 0 || process.env.PORTLESS_TLD !== void 0,
5914
6082
  useWildcard: args.includes("--wildcard") || process.env.PORTLESS_WILDCARD !== void 0
5915
6083
  };
5916
6084
  let stateDir = resolveStateDir(proxyPort);
@@ -5928,13 +6096,13 @@ ${colors_default.bold("LAN mode (--lan):")}
5928
6096
  const desiredConfig = resolveProxyConfig({
5929
6097
  persistedLanMode,
5930
6098
  explicit,
5931
- defaultTld: getDefaultTld(),
6099
+ defaultTlds: getDefaultTlds(),
5932
6100
  useHttps: wantHttps || !!(customCertPath && customKeyPath),
5933
6101
  customCertPath,
5934
6102
  customKeyPath,
5935
6103
  lanMode: isLanEnvEnabled(),
5936
6104
  lanIp: process.env.PORTLESS_LAN_IP || null,
5937
- tld,
6105
+ tlds,
5938
6106
  useWildcard
5939
6107
  });
5940
6108
  const lanMode = desiredConfig.lanMode;
@@ -5942,31 +6110,35 @@ ${colors_default.bold("LAN mode (--lan):")}
5942
6110
  customCertPath = desiredConfig.customCertPath;
5943
6111
  customKeyPath = desiredConfig.customKeyPath;
5944
6112
  tld = desiredConfig.tld;
6113
+ tlds = desiredConfig.tlds;
5945
6114
  const desiredWildcard = desiredConfig.useWildcard;
5946
6115
  let lanIp = desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null;
5947
6116
  if (!hasExplicitPort && runningPort === null) {
5948
6117
  proxyPort = getDefaultPort(useHttps);
5949
6118
  stateDir = resolveStateDir(proxyPort);
5950
6119
  }
5951
- if (lanMode && tldIdx !== -1) {
5952
- const userTld = args[tldIdx + 1];
5953
- if (userTld && userTld !== "local") {
6120
+ if (lanMode && tldFlagValues.length > 0) {
6121
+ const userTlds = tldFlagValues.join(", ");
6122
+ if (userTlds !== "local") {
5954
6123
  console.warn(
5955
6124
  chalk.yellow(
5956
- `Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTld}.`
6125
+ `Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTlds}.`
5957
6126
  )
5958
6127
  );
5959
6128
  }
5960
6129
  }
5961
- const riskyReason = RISKY_TLDS.get(tld);
5962
- if (riskyReason && !lanMode) {
5963
- console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
6130
+ for (const configuredTld of tlds) {
6131
+ const riskyReason = RISKY_TLDS.get(configuredTld);
6132
+ if (riskyReason && !lanMode) {
6133
+ console.warn(colors_default.yellow(`Warning: .${configuredTld}: ${riskyReason}`));
6134
+ }
5964
6135
  }
5965
6136
  const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
5966
- if (tld !== DEFAULT_TLD && !lanMode && syncDisabled) {
6137
+ const nonDefaultTlds = tlds.filter((configuredTld) => configuredTld !== DEFAULT_TLD);
6138
+ if (nonDefaultTlds.length > 0 && !lanMode && syncDisabled) {
5967
6139
  console.warn(
5968
6140
  colors_default.yellow(
5969
- `Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
6141
+ `Warning: ${formatTldList2(nonDefaultTlds)} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
5970
6142
  )
5971
6143
  );
5972
6144
  console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
@@ -6028,6 +6200,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6028
6200
  lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
6029
6201
  lanIpExplicit: desiredConfig.lanIpExplicit,
6030
6202
  tld,
6203
+ tlds,
6031
6204
  useWildcard: desiredWildcard
6032
6205
  };
6033
6206
  if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
@@ -6044,6 +6217,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6044
6217
  lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
6045
6218
  lanIpExplicit: desiredConfig.lanIpExplicit,
6046
6219
  tld,
6220
+ tlds,
6047
6221
  useWildcard: desiredWildcard,
6048
6222
  foreground: isForeground,
6049
6223
  includePort: true,
@@ -6161,7 +6335,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6161
6335
  cert,
6162
6336
  key,
6163
6337
  ca,
6164
- SNICallback: createSNICallback(stateDir, cert, key, tld, ca)
6338
+ SNICallback: createSNICallback(stateDir, cert, key, tlds, ca)
6165
6339
  };
6166
6340
  }
6167
6341
  }
@@ -6171,6 +6345,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6171
6345
  store,
6172
6346
  proxyPort,
6173
6347
  tld,
6348
+ tlds,
6174
6349
  tlsOptions,
6175
6350
  lanIp,
6176
6351
  desiredWildcard ? false : void 0,
@@ -6199,6 +6374,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6199
6374
  lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
6200
6375
  lanIpExplicit: desiredConfig.lanIpExplicit,
6201
6376
  tld,
6377
+ tlds,
6202
6378
  useWildcard: desiredWildcard,
6203
6379
  foreground: true,
6204
6380
  includePort: true,
@@ -6292,7 +6468,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
6292
6468
  }
6293
6469
  const worktree = detectWorktreePrefix(cwd);
6294
6470
  const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
6295
- const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
6471
+ const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6296
6472
  const store = new RouteStore(dir, {
6297
6473
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6298
6474
  });
@@ -6303,7 +6479,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
6303
6479
  effectiveName,
6304
6480
  resolved,
6305
6481
  tls2,
6306
- tld,
6482
+ tlds,
6307
6483
  false,
6308
6484
  { nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
6309
6485
  appConfig?.appPort,
@@ -6343,13 +6519,13 @@ function pipeOutput(child, prefix) {
6343
6519
  prefixStream(child.stdout, process.stdout, prefix);
6344
6520
  prefixStream(child.stderr, process.stderr, prefix);
6345
6521
  }
6346
- async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6522
+ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes) {
6347
6523
  const usesPortless = app.commandArgs[0] === "portless";
6348
6524
  const pkgEnv = { ...process.env };
6349
6525
  pkgEnv.PATH = augmentedPath(pkgEnv, app.pkg.dir);
6350
6526
  let env;
6351
6527
  let store = null;
6352
- let hostname = null;
6528
+ let hostnames = [];
6353
6529
  let displayUrl;
6354
6530
  if (usesPortless) {
6355
6531
  env = pkgEnv;
@@ -6359,17 +6535,17 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6359
6535
  onWarning: (msg) => console.warn(colors_default.yellow(`[${app.name}] ${msg}`))
6360
6536
  });
6361
6537
  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}`;
6538
+ hostnames = buildHostnames(app.name, tlds);
6539
+ const urls = formatUrls(hostnames, proxyPort, tls2);
6540
+ const url = urls[0];
6365
6541
  displayUrl = url;
6366
- hostname = parseHostname(app.name, tld);
6367
- store.addRoute(hostname, appPort, process.pid);
6542
+ addRoutes(store, hostnames, appPort, process.pid);
6368
6543
  env = {
6369
6544
  ...pkgEnv,
6370
6545
  PORT: String(appPort),
6371
6546
  HOST: "127.0.0.1",
6372
- PORTLESS_URL: url
6547
+ PORTLESS_URL: url,
6548
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds)
6373
6549
  };
6374
6550
  if (tls2) {
6375
6551
  const caPath = path9.join(stateDir, "ca.pem");
@@ -6381,7 +6557,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6381
6557
  const child = spawnChildProcess(app.commandArgs, env, app.pkg.dir);
6382
6558
  pipeOutput(child, chalk.cyan(`[${app.name}]`));
6383
6559
  const capturedStore = store;
6384
- const capturedHostname = hostname;
6560
+ const capturedHostnames = hostnames;
6385
6561
  child.on("exit", (code, signal) => {
6386
6562
  exitCodes.set(app.name, code);
6387
6563
  if (code !== 0 && code !== null) {
@@ -6389,14 +6565,11 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
6389
6565
  } else if (signal) {
6390
6566
  console.error(colors_default.yellow(`[${app.name}] killed by ${signal}`));
6391
6567
  }
6392
- if (capturedStore && capturedHostname) {
6393
- try {
6394
- capturedStore.removeRoute(capturedHostname, process.pid);
6395
- } catch {
6396
- }
6568
+ if (capturedStore && capturedHostnames.length > 0) {
6569
+ removeRoutes(capturedStore, capturedHostnames, process.pid);
6397
6570
  }
6398
6571
  });
6399
- const route = store && hostname ? { store, hostname } : null;
6572
+ const route = store && hostnames.length > 0 ? { store, hostnames } : null;
6400
6573
  return { child, displayUrl, route };
6401
6574
  }
6402
6575
  function spawnTaskApp(app, exitCodes) {
@@ -6507,7 +6680,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
6507
6680
  console.log(chalk.blue.bold(`
6508
6681
  portless
6509
6682
  `));
6510
- let { dir, port, tls: tls2, tld } = await discoverState();
6683
+ let { dir, port, tls: tls2, tlds } = await discoverState();
6511
6684
  if (proxiedApps.length > 0) {
6512
6685
  let multiDesired;
6513
6686
  try {
@@ -6521,9 +6694,9 @@ portless
6521
6694
  dir = ensureResult.state.dir;
6522
6695
  port = ensureResult.state.port;
6523
6696
  tls2 = ensureResult.state.tls;
6524
- tld = ensureResult.state.tld;
6697
+ tlds = ensureResult.state.tlds;
6525
6698
  } else {
6526
- ({ dir, port, tls: tls2, tld } = await discoverState());
6699
+ ({ dir, port, tls: tls2, tlds } = await discoverState());
6527
6700
  }
6528
6701
  if (tls2 && !isCATrusted(dir)) {
6529
6702
  await handleTrust();
@@ -6531,12 +6704,12 @@ portless
6531
6704
  }
6532
6705
  const useTurbo = loaded?.config.turbo !== false && hasTurboConfig(wsRoot);
6533
6706
  if (useTurbo) {
6534
- await runWithTurbo(wsRoot, dir, port, tls2, tld, scriptName, proxiedApps, taskApps, extraArgs);
6707
+ await runWithTurbo(wsRoot, dir, port, tls2, tlds, scriptName, proxiedApps, taskApps, extraArgs);
6535
6708
  } else {
6536
- await runWithDirectSpawn(dir, port, tls2, tld, proxiedApps, taskApps);
6709
+ await runWithDirectSpawn(dir, port, tls2, tlds, proxiedApps, taskApps);
6537
6710
  }
6538
6711
  }
6539
- async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName, proxiedApps, taskApps, extraArgs = []) {
6712
+ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName, proxiedApps, taskApps, extraArgs = []) {
6540
6713
  const store = new RouteStore(stateDir, {
6541
6714
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6542
6715
  });
@@ -6550,17 +6723,17 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
6550
6723
  continue;
6551
6724
  }
6552
6725
  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}`;
6726
+ const hostnames = buildHostnames(app.name, tlds);
6727
+ const urls = formatUrls(hostnames, proxyPort, tls2);
6728
+ const url = urls[0];
6556
6729
  appUrls.push({ label: app.label, url });
6557
- const hostname = parseHostname(app.name, tld);
6558
- store.addRoute(hostname, appPort, process.pid);
6559
- routes.push({ hostname });
6730
+ addRoutes(store, hostnames, appPort, process.pid);
6731
+ routes.push({ hostnames });
6560
6732
  const entry = {
6561
6733
  PORT: String(appPort),
6562
6734
  HOST: "127.0.0.1",
6563
- PORTLESS_URL: url
6735
+ PORTLESS_URL: url,
6736
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds)
6564
6737
  };
6565
6738
  if (tls2) {
6566
6739
  const caPath = path9.join(stateDir, "ca.pem");
@@ -6603,11 +6776,8 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
6603
6776
  killTree(turboChild, "SIGKILL");
6604
6777
  }
6605
6778
  }, SIGKILL_TIMEOUT_MS).unref();
6606
- for (const { hostname } of routes) {
6607
- try {
6608
- store.removeRoute(hostname, process.pid);
6609
- } catch {
6610
- }
6779
+ for (const { hostnames } of routes) {
6780
+ removeRoutes(store, hostnames, process.pid);
6611
6781
  }
6612
6782
  removeManifest();
6613
6783
  };
@@ -6621,7 +6791,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
6621
6791
  process.exit(exitCode);
6622
6792
  }
6623
6793
  }
6624
- async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, taskApps) {
6794
+ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tlds, proxiedApps, taskApps) {
6625
6795
  const children = [];
6626
6796
  const exitCodes = /* @__PURE__ */ new Map();
6627
6797
  const appUrls = [];
@@ -6632,7 +6802,7 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
6632
6802
  stateDir,
6633
6803
  proxyPort,
6634
6804
  tls2,
6635
- tld,
6805
+ tlds,
6636
6806
  exitCodes
6637
6807
  );
6638
6808
  children.push(child);
@@ -6667,11 +6837,8 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
6667
6837
  }
6668
6838
  }
6669
6839
  }, SIGKILL_TIMEOUT_MS).unref();
6670
- for (const { store, hostname } of routeEntries) {
6671
- try {
6672
- store.removeRoute(hostname, process.pid);
6673
- } catch {
6674
- }
6840
+ for (const { store, hostnames } of routeEntries) {
6841
+ removeRoutes(store, hostnames, process.pid);
6675
6842
  }
6676
6843
  };
6677
6844
  process.on("SIGINT", cleanup);
@@ -6730,7 +6897,7 @@ async function handleRunMode(args, globalScript) {
6730
6897
  }
6731
6898
  const worktree = detectWorktreePrefix();
6732
6899
  const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
6733
- const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
6900
+ const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6734
6901
  const store = new RouteStore(dir, {
6735
6902
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6736
6903
  });
@@ -6741,7 +6908,7 @@ async function handleRunMode(args, globalScript) {
6741
6908
  effectiveName,
6742
6909
  parsed.commandArgs,
6743
6910
  tls2,
6744
- tld,
6911
+ tlds,
6745
6912
  parsed.force,
6746
6913
  { nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
6747
6914
  parsed.appPort,
@@ -6767,7 +6934,7 @@ async function handleNamedMode(args) {
6767
6934
  }
6768
6935
  const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
6769
6936
  parseHostname(safeName, DEFAULT_TLD);
6770
- const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
6937
+ const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6771
6938
  const store = new RouteStore(dir, {
6772
6939
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
6773
6940
  });
@@ -6778,7 +6945,7 @@ async function handleNamedMode(args) {
6778
6945
  safeName,
6779
6946
  parsed.commandArgs,
6780
6947
  tls2,
6781
- tld,
6948
+ tlds,
6782
6949
  parsed.force,
6783
6950
  void 0,
6784
6951
  parsed.appPort,