@linxin666/dsh-remote-web-ui 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +3 -3
  3. package/README.zh.md +3 -3
  4. package/lib/client.js +55 -21
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +174 -39
  7. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  8. package/lib/types/client/RemoteEntry.js +11 -1
  9. package/lib/types/client/RemotePanel.d.ts +5 -1
  10. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  11. package/lib/types/client/RemotePanel.js +2 -2
  12. package/lib/types/client/locales.d.ts +8 -0
  13. package/lib/types/client/locales.d.ts.map +1 -1
  14. package/lib/types/client/locales.js +8 -0
  15. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  16. package/lib/types/client/mobile-adapt.js +7 -12
  17. package/lib/types/client/remote-channel.d.ts +0 -2
  18. package/lib/types/client/remote-channel.d.ts.map +1 -1
  19. package/lib/types/client/remote-channel.js +0 -4
  20. package/lib/types/index.d.ts +9 -1
  21. package/lib/types/index.d.ts.map +1 -1
  22. package/lib/types/index.js +54 -27
  23. package/lib/types/loopback-proxy.d.ts.map +1 -1
  24. package/lib/types/loopback-proxy.js +16 -0
  25. package/lib/types/remote-api.d.ts +1 -1
  26. package/lib/types/remote-api.d.ts.map +1 -1
  27. package/lib/types/remote-api.js +1 -1
  28. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  29. package/lib/types/remote-channel-boot.js +0 -1
  30. package/lib/types/remote-channel-rules.d.ts +0 -1
  31. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  32. package/lib/types/remote-channel-rules.js +0 -1
  33. package/lib/types/remote-methods.d.ts +1 -4
  34. package/lib/types/remote-methods.d.ts.map +1 -1
  35. package/lib/types/remote-methods.js +1 -5
  36. package/lib/types/routes.d.ts +26 -2
  37. package/lib/types/routes.d.ts.map +1 -1
  38. package/lib/types/routes.js +150 -5
  39. package/package.json +18 -18
  40. package/src/client/RemoteEntry.tsx +12 -0
  41. package/src/client/RemotePanel.tsx +37 -2
  42. package/src/client/locales.ts +8 -0
  43. package/src/client/mobile-adapt.ts +5 -10
  44. package/src/client/remote-channel.ts +0 -3
  45. package/src/index.ts +59 -26
  46. package/src/loopback-proxy.ts +15 -0
  47. package/src/remote-api.ts +0 -1
  48. package/src/remote-channel-boot.ts +0 -1
  49. package/src/remote-channel-rules.ts +0 -2
  50. package/src/remote-methods.ts +1 -6
  51. package/src/routes.ts +141 -7
package/lib/index.js CHANGED
@@ -724,6 +724,66 @@ function isTrustedApiRequest(request, trustedHosts) {
724
724
  return false;
725
725
  }
726
726
  }
727
+ /**
728
+ * Test whether a hostname represents a private local-area network (RFC 1918 / ULA / mDNS / loopback).
729
+ * Supports pairing in container-bridged (Docker) or NAT-proxied topologies where the host
730
+ * machine's LAN IP or proxy domain differs from the container's internal sampled network interface.
731
+ */
732
+ function isPrivateOrLocalHostname(hostname) {
733
+ const normalized = hostname.toLowerCase().trim();
734
+ if (normalized === "localhost" || normalized.endsWith(".local") || normalized.endsWith(".lan") || normalized.endsWith(".internal") || normalized.endsWith(".home.arpa")) return true;
735
+ let ip = normalized;
736
+ if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
737
+ if (ip === "::1" || ip === "127.0.0.1") return true;
738
+ if (ip.startsWith("fc") || ip.startsWith("fd") || ip.startsWith("fe80")) return true;
739
+ const parts = ip.split(".").map(Number);
740
+ if (parts.length === 4 && parts.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)) {
741
+ const [a, b] = parts;
742
+ if (a === 10) return true;
743
+ if (a === 127) return true;
744
+ if (a === 169 && b === 254) return true;
745
+ if (a === 172 && b >= 16 && b <= 31) return true;
746
+ if (a === 192 && b === 168) return true;
747
+ }
748
+ return false;
749
+ }
750
+ /**
751
+ * Validates that an incoming request is non-cross-site (rejects cross-site fetch metadata
752
+ * and ensures Origin matches Host when present).
753
+ */
754
+ function isNonCrossSite(request) {
755
+ if (request.headers["sec-fetch-site"] === "cross-site") return false;
756
+ const host = request.headers.host;
757
+ if (typeof host !== "string") return false;
758
+ const origin = request.headers.origin;
759
+ if (origin === void 0) return true;
760
+ try {
761
+ const originUrl = new URL(origin);
762
+ const hostUrl = new URL(`http://${host}`);
763
+ return originUrl.host === hostUrl.host;
764
+ } catch {
765
+ return false;
766
+ }
767
+ }
768
+ /**
769
+ * The private-LAN fallback authority of a request: its Host header when the
770
+ * hostname is private/local (RFC 1918 / ULA / mDNS / loopback) and the
771
+ * browser markers are non-cross-site, else undefined. The accept paths and
772
+ * the lanFence fallback use this to serve container-bridged and reverse-proxy
773
+ * topologies whose Host is not among the advertised or configured authorities.
774
+ */
775
+ function privateLanHostOf(request) {
776
+ const host = request.headers.host;
777
+ if (typeof host !== "string") return void 0;
778
+ let hostName = "";
779
+ try {
780
+ hostName = new URL(`http://${host}`).hostname;
781
+ } catch {
782
+ return;
783
+ }
784
+ if (hostName === "" || !isPrivateOrLocalHostname(hostName) || !isNonCrossSite(request)) return void 0;
785
+ return host;
786
+ }
727
787
  /** Cap on pairing request bodies (tokens and workspace ids are tiny). */
728
788
  const MAX_BODY_BYTES = 4096;
729
789
  /**
@@ -740,6 +800,20 @@ function acceptLimitKey(socketIp, forwarded, bucket) {
740
800
  return `${bucket}|${socketIp}`;
741
801
  }
742
802
  /**
803
+ * FIFO-bounded Set insert: past `max` entries the oldest one is evicted
804
+ * (a Set iterates in insertion order). The dynamic trusted-host table is fed
805
+ * by caller-controlled Host headers, so it must stay bounded; an evicted
806
+ * legitimate host re-adds itself on that device's next gated request.
807
+ */
808
+ function addBounded(set, value, max) {
809
+ if (set.has(value)) return;
810
+ if (set.size >= max) {
811
+ const oldest = set.values().next().value;
812
+ if (oldest !== void 0) set.delete(oldest);
813
+ }
814
+ set.add(value);
815
+ }
816
+ /**
743
817
  * The host authority of a configured public base URL, e.g. `foo.trycloudflare.com`
744
818
  * from `https://foo.trycloudflare.com`. Undefined when the URL does not parse —
745
819
  * a malformed config then simply contributes no fence entry (and the panel
@@ -973,16 +1047,37 @@ var PairingEventsStream = class {
973
1047
  * @returns the exact routes to register on webServer.
974
1048
  */
975
1049
  function makeRoutes(deps) {
976
- const { service, lanAddresses, requirePairingForLan = true } = deps;
1050
+ const { service, requirePairingForLan = true } = deps;
977
1051
  const pairingRequired = () => typeof requirePairingForLan === "function" ? requirePairingForLan() : requirePairingForLan;
978
1052
  const events = new PairingEventsStream(service);
1053
+ const dynamicTrustedHosts = /* @__PURE__ */ new Set();
979
1054
  /** Loopback-only fence: the desktop panel's control endpoints. */
980
1055
  const loopbackFence = (req) => isTrustedApiRequest(req, []);
981
- /** Phone-facing fence: loopback, the service's live LAN literals, or the configured public host. */
1056
+ /** Phone-facing fence: loopback, the service's live LAN literals, configured public host, extra trusted hosts, or dynamically paired hosts. */
982
1057
  const lanFence = (req) => {
983
1058
  const publicHost = publicHostOf(service.publicBaseUrl);
984
1059
  const bases = service.lanAddresses;
985
- return isTrustedApiRequest(req, publicHost === void 0 ? bases : [...bases, publicHost]);
1060
+ const extraHosts = typeof deps.trustedHosts === "function" ? deps.trustedHosts() : deps.trustedHosts ?? [];
1061
+ if (isTrustedApiRequest(req, [
1062
+ ...bases,
1063
+ ...publicHost !== void 0 ? [publicHost] : [],
1064
+ ...extraHosts,
1065
+ ...dynamicTrustedHosts
1066
+ ])) return true;
1067
+ const privateLanHost = privateLanHostOf(req);
1068
+ if (privateLanHost !== void 0) {
1069
+ const cookieDeviceId = readCookie(req.headers.cookie, service.config.cookieName);
1070
+ let queryDeviceId = null;
1071
+ try {
1072
+ queryDeviceId = new URL(req.url ?? "/", "http://pair.invalid").searchParams.get("device");
1073
+ } catch {}
1074
+ const deviceId = cookieDeviceId ?? queryDeviceId ?? void 0;
1075
+ if (deviceId !== void 0 && service.hasDevice(deviceId)) {
1076
+ addBounded(dynamicTrustedHosts, privateLanHost, 64);
1077
+ return true;
1078
+ }
1079
+ }
1080
+ return false;
986
1081
  };
987
1082
  const requireMethod = (req, res, method) => {
988
1083
  if (req.method === method) return true;
@@ -1060,7 +1155,8 @@ function makeRoutes(deps) {
1060
1155
  };
1061
1156
  const handleAccept = async (req, res) => {
1062
1157
  if (!requireMethod(req, res, "POST")) return;
1063
- if (!lanFence(req)) {
1158
+ const privateLanHost = privateLanHostOf(req);
1159
+ if (!lanFence(req) && privateLanHost === void 0) {
1064
1160
  writeJson(res, 403, {
1065
1161
  ok: false,
1066
1162
  code: "forbidden"
@@ -1089,12 +1185,20 @@ function makeRoutes(deps) {
1089
1185
  const ua = req.headers["user-agent"];
1090
1186
  const result = service.accept(payload.token, typeof ua === "string" ? ua : void 0);
1091
1187
  if (!result.ok) {
1188
+ if (!lanFence(req)) {
1189
+ writeJson(res, 403, {
1190
+ ok: false,
1191
+ code: "forbidden"
1192
+ });
1193
+ return;
1194
+ }
1092
1195
  writeJson(res, result.code === "used" ? 409 : 404, {
1093
1196
  ok: false,
1094
1197
  code: result.code
1095
1198
  });
1096
1199
  return;
1097
1200
  }
1201
+ if (privateLanHost !== void 0) addBounded(dynamicTrustedHosts, privateLanHost, 64);
1098
1202
  writeJson(res, 200, {
1099
1203
  ok: true,
1100
1204
  deviceId: result.deviceId
@@ -1245,7 +1349,8 @@ function makeRoutes(deps) {
1245
1349
  */
1246
1350
  const handleAcceptPage = async (req, res) => {
1247
1351
  if (!requireMethod(req, res, "GET")) return;
1248
- if (!lanFence(req)) {
1352
+ const privateLanHost = privateLanHostOf(req);
1353
+ if (!lanFence(req) && privateLanHost === void 0) {
1249
1354
  res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
1250
1355
  res.end("forbidden");
1251
1356
  return;
@@ -1262,6 +1367,11 @@ function makeRoutes(deps) {
1262
1367
  code: "invalid"
1263
1368
  } : service.accept(token, typeof ua === "string" ? ua : void 0);
1264
1369
  if (!result.ok) {
1370
+ if (!lanFence(req)) {
1371
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
1372
+ res.end("forbidden");
1373
+ return;
1374
+ }
1265
1375
  const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
1266
1376
  if (deviceId !== void 0 && service.hasDevice(deviceId)) {
1267
1377
  res.writeHead(303, {
@@ -1280,6 +1390,7 @@ function makeRoutes(deps) {
1280
1390
  res.end(pairingFailurePage());
1281
1391
  return;
1282
1392
  }
1393
+ if (privateLanHost !== void 0) addBounded(dynamicTrustedHosts, privateLanHost, 64);
1283
1394
  res.writeHead(303, {
1284
1395
  location: `${appOrigin(req)}/pair-app?device=${encodeURIComponent(result.deviceId)}`,
1285
1396
  "cache-control": "no-store",
@@ -1484,6 +1595,12 @@ function pipeLoopbackHttp(req, res, port, upstreamPath, auth, cookie) {
1484
1595
  }
1485
1596
  res.writeHead(upstreamRes.statusCode ?? 502, out);
1486
1597
  upstreamRes.pipe(res);
1598
+ upstreamRes.on("error", () => {
1599
+ res.destroy();
1600
+ });
1601
+ res.on("close", () => {
1602
+ if (!upstreamRes.readableEnded) upstream.destroy();
1603
+ });
1487
1604
  });
1488
1605
  upstream.on("error", () => {
1489
1606
  if (!res.headersSent) {
@@ -1498,6 +1615,9 @@ function pipeLoopbackHttp(req, res, port, upstreamPath, auth, cookie) {
1498
1615
  }
1499
1616
  res.destroy();
1500
1617
  });
1618
+ req.on("error", () => {
1619
+ upstream.destroy();
1620
+ });
1501
1621
  req.pipe(upstream);
1502
1622
  }
1503
1623
  /**
@@ -1577,8 +1697,6 @@ const REMOTE_UPGRADE_PATHS = [
1577
1697
  ];
1578
1698
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
1579
1699
  const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
1580
- /** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
1581
- const DESKTOP_LAUNCHER_PATH = "/api/dsh-desktop-launcher";
1582
1700
  /**
1583
1701
  * The cookieless device credential: the boot patch reads the device id from
1584
1702
  * the /pair-app URL, keeps it in sessionStorage, and attaches it to every
@@ -1595,14 +1713,12 @@ const REMOTE_DEVICE_QUERY = "device";
1595
1713
  * paired remote desktop may use the full host API (chat, sessions,
1596
1714
  * settings, credentials, presets — it is a full-control credential), but it
1597
1715
  * must not reach the machine-control planes: pairing control itself, the
1598
- * dsh-web self-update installer, plugin install/remove, and desktop
1599
- * launcher actions (host shutdown, shortcuts).
1716
+ * dsh-web self-update installer, and plugin install/remove.
1600
1717
  */
1601
1718
  const LOCAL_ONLY_PREFIXES = [
1602
1719
  "/api/pair",
1603
1720
  "/api/update",
1604
- PLUGIN_MANAGER_PATH,
1605
- DESKTOP_LAUNCHER_PATH
1721
+ PLUGIN_MANAGER_PATH
1606
1722
  ];
1607
1723
  /**
1608
1724
  * Whether a paired inner path must stay physically local.
@@ -3580,7 +3696,6 @@ const REMOTE_CHANNEL_RULES = {
3580
3696
  apiPrefix: "/api/",
3581
3697
  pairPrefix: "/api/pair/",
3582
3698
  updatePrefix: "/api/update/",
3583
- desktopLauncherPrefix: "/api/dsh-desktop-launcher",
3584
3699
  settingsBridgePrefix: "/api/dsh-web-ui-settings",
3585
3700
  sidebarPrefix: "/sidebar/",
3586
3701
  gitPrefix: "/git/",
@@ -3630,7 +3745,7 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
3630
3745
  function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
3631
3746
  const json = JSON.stringify(rules);
3632
3747
  const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
3633
- return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.desktopLauncherPrefix||p.indexOf(R.desktopLauncherPrefix+\"/\")===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
3748
+ return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
3634
3749
  }
3635
3750
  /** Boot-watchdog latch: one self-reload per session while the boot is broken. */
3636
3751
  const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
@@ -3715,6 +3830,7 @@ const Config = z.object({
3715
3830
  cookieName: z.string().min(1).default("dsh_pair"),
3716
3831
  requirePairingForLan: z.boolean().default(true),
3717
3832
  publicBaseUrl: z.string(),
3833
+ trustedHosts: z.array(z.string()),
3718
3834
  devicesFile: z.string(),
3719
3835
  autoTunnel: z.boolean().default(false),
3720
3836
  tunnelToken: z.string().role("secret"),
@@ -3754,6 +3870,7 @@ const DEFAULTS = {
3754
3870
  cookieName: "dsh_pair",
3755
3871
  requirePairingForLan: true,
3756
3872
  publicBaseUrl: void 0,
3873
+ trustedHosts: void 0,
3757
3874
  devicesFile: defaultDevicesFile(),
3758
3875
  autoTunnel: false,
3759
3876
  tunnelToken: void 0,
@@ -3769,6 +3886,7 @@ const DEFAULTS = {
3769
3886
  */
3770
3887
  const apply = mountOnce("@linxin666/dsh-remote-web-ui", applyImpl);
3771
3888
  function applyImpl(ctx, config) {
3889
+ const envPublicBase = process.env.DSH_REMOTE_PUBLIC_BASE_URL?.trim() || void 0;
3772
3890
  const resolved = {
3773
3891
  tokenTtlMs: config?.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
3774
3892
  offlineAfterMs: config?.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
@@ -3776,7 +3894,8 @@ function applyImpl(ctx, config) {
3776
3894
  idleExpireMs: config?.idleExpireMs ?? DEFAULTS.idleExpireMs,
3777
3895
  cookieName: config?.cookieName ?? DEFAULTS.cookieName,
3778
3896
  requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
3779
- publicBaseUrl: config?.publicBaseUrl,
3897
+ publicBaseUrl: config?.publicBaseUrl ?? envPublicBase,
3898
+ trustedHosts: config?.trustedHosts,
3780
3899
  devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
3781
3900
  autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
3782
3901
  tunnelToken: config?.tunnelToken,
@@ -3795,7 +3914,8 @@ function applyImpl(ctx, config) {
3795
3914
  idleExpireMs: value.idleExpireMs ?? DEFAULTS.idleExpireMs,
3796
3915
  cookieName: value.cookieName ?? DEFAULTS.cookieName,
3797
3916
  requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
3798
- publicBaseUrl: value.publicBaseUrl,
3917
+ publicBaseUrl: value.publicBaseUrl ?? envPublicBase,
3918
+ trustedHosts: value.trustedHosts,
3799
3919
  devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
3800
3920
  autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
3801
3921
  tunnelToken: value.tunnelToken,
@@ -3885,13 +4005,15 @@ function applyImpl(ctx, config) {
3885
4005
  let disposeRoutes;
3886
4006
  let disposeSweep;
3887
4007
  const requireFromHost = createRequire(import.meta.url);
3888
- const resolveAnchorPath = () => resolveAnchorManifest((specifier) => {
4008
+ /** Host-process resolve that degrades to "not installed" (undefined) instead of throwing. */
4009
+ const hostResolve = (specifier) => {
3889
4010
  try {
3890
4011
  return requireFromHost.resolve(specifier);
3891
4012
  } catch {
3892
4013
  return;
3893
4014
  }
3894
- });
4015
+ };
4016
+ const resolveAnchorPath = () => resolveAnchorManifest(hostResolve);
3895
4017
  const releaseNotesCache = /* @__PURE__ */ new Map();
3896
4018
  const fetchReleaseNotesCached = async (version) => {
3897
4019
  const cached = releaseNotesCache.get(version);
@@ -3907,13 +4029,7 @@ function applyImpl(ctx, config) {
3907
4029
  fence: (request) => isTrustedApiRequest(request, []),
3908
4030
  check: () => checkUpdates({
3909
4031
  anchorManifestPath: resolveAnchorPath(),
3910
- resolve: (specifier) => {
3911
- try {
3912
- return requireFromHost.resolve(specifier);
3913
- } catch {
3914
- return;
3915
- }
3916
- },
4032
+ resolve: hostResolve,
3917
4033
  fetchLatest: (name) => fetchLatestVersion(name, fetch),
3918
4034
  fetchReleaseNotes: fetchReleaseNotesCached
3919
4035
  }),
@@ -3936,13 +4052,7 @@ function applyImpl(ctx, config) {
3936
4052
  },
3937
4053
  check: {
3938
4054
  anchorManifestPath: resolveAnchorPath(),
3939
- resolve: (specifier) => {
3940
- try {
3941
- return requireFromHost.resolve(specifier);
3942
- } catch {
3943
- return;
3944
- }
3945
- },
4055
+ resolve: hostResolve,
3946
4056
  fetchLatest: (name) => fetchLatestVersion(name, fetch),
3947
4057
  fetchReleaseNotes: fetchReleaseNotesCached
3948
4058
  }
@@ -4009,10 +4119,25 @@ function applyImpl(ctx, config) {
4009
4119
  const routes = [
4010
4120
  ...makeRoutes({
4011
4121
  service,
4012
- lanAddresses: service.lanAddresses,
4013
4122
  requirePairingForLan: () => resolve().requirePairingForLan,
4014
4123
  lanBindStatus,
4015
- indexDocument: fetchAppShell
4124
+ indexDocument: fetchAppShell,
4125
+ trustedHosts: () => {
4126
+ const list = [];
4127
+ const envHosts = process.env.DSH_REMOTE_TRUSTED_HOSTS;
4128
+ if (typeof envHosts === "string" && envHosts.trim() !== "") for (const item of envHosts.split(",")) {
4129
+ const trimmed = item.trim();
4130
+ if (trimmed !== "") list.push(trimmed);
4131
+ }
4132
+ const cfgHosts = resolve().trustedHosts;
4133
+ if (Array.isArray(cfgHosts)) {
4134
+ for (const item of cfgHosts) if (typeof item === "string") {
4135
+ const trimmed = item.trim();
4136
+ if (trimmed !== "") list.push(trimmed);
4137
+ }
4138
+ }
4139
+ return list;
4140
+ }
4016
4141
  }),
4017
4142
  ...makeRemoteApiRoutes({
4018
4143
  service,
@@ -4191,13 +4316,23 @@ function applyImpl(ctx, config) {
4191
4316
  });
4192
4317
  }), "remote-web-ui: remote channel boot patch");
4193
4318
  ctx.inject(["settings"], (settingsCtx) => {
4194
- settingsCtx.settings.installSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
4195
- setSource: (source) => {
4196
- current = source;
4319
+ try {
4320
+ if (typeof settingsCtx.settings?.installSection === "function") settingsCtx.settings.installSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
4321
+ setSource: (source) => {
4322
+ current = source;
4323
+ sync();
4324
+ },
4325
+ onChange: sync
4326
+ });
4327
+ else if (typeof settingsCtx.settings?.register === "function") {
4328
+ const scope = settingsCtx.settings.register(REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, { base: config ?? {} });
4329
+ current = () => scope?.get?.() ?? config ?? {};
4330
+ scope?.watch?.(() => {
4331
+ sync();
4332
+ });
4197
4333
  sync();
4198
- },
4199
- onChange: sync
4200
- });
4334
+ }
4335
+ } catch {}
4201
4336
  });
4202
4337
  sync();
4203
4338
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteEntry.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteEntry.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAQnE,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG;IACrD,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AA2BD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA2LxD"}
1
+ {"version":3,"file":"RemoteEntry.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteEntry.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAQnE,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG;IACrD,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AA2BD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BAuMxD"}
@@ -54,6 +54,7 @@ export function RemoteEntry({ wide, t }) {
54
54
  const stateRef = useRef(state);
55
55
  useEffect(() => { stateRef.current = state; }, [state]);
56
56
  const [copied, setCopied] = useState(false);
57
+ const [copiedToken, setCopiedToken] = useState(false);
57
58
  const eventSource = useRef(undefined);
58
59
  // Generation counter for the open flow: closing (or re-opening) the panel
59
60
  // bumps it, so an in-flight issue() that resolves after a close does not
@@ -87,6 +88,7 @@ export function RemoteEntry({ wide, t }) {
87
88
  return {
88
89
  kind: 'ready',
89
90
  url: result.url,
91
+ token: result.token,
90
92
  expiresAt: result.expiresAt,
91
93
  expired: Date.now() > result.expiresAt,
92
94
  phase: 'waiting',
@@ -202,7 +204,15 @@ export function RemoteEntry({ wide, t }) {
202
204
  window.setTimeout(() => { setCopied(false); }, 1500);
203
205
  });
204
206
  }, []);
205
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.entryRow, "data-rail": wide ? undefined : 'rail', children: [_jsx(UpdateEntry, { wide: wide, t: t }), _jsx(TooltipAnchor, { wide: wide, label: t('entry.label'), onClick: openPanel, expanded: open })] }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(RemotePanel, { t: t, state: state, copied: copied, onClose: closePanel, onStop: handleStop, onRefresh: handleRefresh, onCopy: handleCopy, onPickAddress: handlePickAddress, onPickPublic: handlePickPublic, onRevoke: handleRevoke })] })), document.body)] }));
207
+ const handleCopyToken = useCallback((token) => {
208
+ void copyText(token).then((ok) => {
209
+ if (!ok)
210
+ return;
211
+ setCopiedToken(true);
212
+ window.setTimeout(() => { setCopiedToken(false); }, 1500);
213
+ });
214
+ }, []);
215
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.entryRow, "data-rail": wide ? undefined : 'rail', children: [_jsx(UpdateEntry, { wide: wide, t: t }), _jsx(TooltipAnchor, { wide: wide, label: t('entry.label'), onClick: openPanel, expanded: open })] }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(RemotePanel, { t: t, state: state, copied: copied, copiedToken: copiedToken, onClose: closePanel, onStop: handleStop, onRefresh: handleRefresh, onCopy: handleCopy, onCopyToken: handleCopyToken, onPickAddress: handlePickAddress, onPickPublic: handlePickPublic, onRevoke: handleRevoke })] })), document.body)] }));
206
216
  }
207
217
  /** The trigger: an icon-only control with a persistent accessible label. */
208
218
  function TooltipAnchor({ wide, label, onClick, expanded }) {
@@ -23,6 +23,8 @@ export type PanelState = {
23
23
  address: string;
24
24
  /** Every constructible LAN literal (interface order). */
25
25
  lanAddresses: string[];
26
+ /** The active pairing token itself (useful in Docker/reverse proxy topologies). */
27
+ token?: string;
26
28
  /** Whether this QR is built on the configured public (tunneled) base. */
27
29
  public: boolean;
28
30
  /** The configured public (tunneled) base URL, when present. */
@@ -39,10 +41,12 @@ export interface RemotePanelProps {
39
41
  t: TranslateNS<'remote'>;
40
42
  state: PanelState;
41
43
  copied: boolean;
44
+ copiedToken?: boolean;
42
45
  onClose(): void;
43
46
  onStop(): void;
44
47
  onRefresh(): void;
45
48
  onCopy(url: string): void;
49
+ onCopyToken?(token: string): void;
46
50
  /** Re-mint the QR against a different LAN address. */
47
51
  onPickAddress(address: string): void;
48
52
  /** Re-mint the QR against the configured public (tunneled) base. */
@@ -55,5 +59,5 @@ export interface RemotePanelProps {
55
59
  * @param props - copy, state, and actions.
56
60
  * @returns the panel element tree.
57
61
  */
58
- export declare function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic, onRevoke }: RemotePanelProps): import("react").JSX.Element;
62
+ export declare function RemotePanel({ t, state, copied, copiedToken, onClose, onStop, onRefresh, onCopy, onCopyToken, onPickAddress, onPickPublic, onRevoke, }: RemotePanelProps): import("react").JSX.Element;
59
63
  //# sourceMappingURL=RemotePanel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RemotePanel.d.ts","sourceRoot":"","sources":["../../../src/client/RemotePanel.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAA;AAItB,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,2DAA2D;IAC3D,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAA;IACf,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,uEAAuE;IACvE,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAEL,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,IAAI,IAAI,CAAA;IACf,MAAM,IAAI,IAAI,CAAA;IACd,SAAS,IAAI,IAAI,CAAA;IACjB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,sDAAsD;IACtD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,oEAAoE;IACpE,YAAY,IAAI,IAAI,CAAA;IACpB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAgBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAuK5I"}
1
+ {"version":3,"file":"RemotePanel.d.ts","sourceRoot":"","sources":["../../../src/client/RemotePanel.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAA;AAItB,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,2DAA2D;IAC3D,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAA;IACf,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,uEAAuE;IACvE,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAEL,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,MAAM,IAAI,IAAI,CAAA;IACd,SAAS,IAAI,IAAI,CAAA;IACjB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,sDAAsD;IACtD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,oEAAoE;IACpE,YAAY,IAAI,IAAI,CAAA;IACpB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAgBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAC1B,CAAC,EACD,KAAK,EACL,MAAM,EACN,WAAW,EACX,OAAO,EACP,MAAM,EACN,SAAS,EACT,MAAM,EACN,WAAW,EACX,aAAa,EACb,YAAY,EACZ,QAAQ,GACT,EAAE,gBAAgB,+BAyLlB"}
@@ -26,10 +26,10 @@ function statusOf(t, state) {
26
26
  * @param props - copy, state, and actions.
27
27
  * @returns the panel element tree.
28
28
  */
29
- export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic, onRevoke }) {
29
+ export function RemotePanel({ t, state, copied, copiedToken, onClose, onStop, onRefresh, onCopy, onCopyToken, onPickAddress, onPickPublic, onRevoke, }) {
30
30
  return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": t('title'), children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: t('title') }), _jsx("p", { className: css.subtitle, children: t('subtitle') })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t('close.label'), onClick: onClose, children: _jsx(IconCloseOutline16, { size: 14 }) })] }), state.kind === 'lan-required' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.lanRequired') }), _jsx("p", { className: css.bannerHint, children: t('status.lanRequiredHint') })] })) : state.kind === 'loopback-required' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.loopbackRequired') }), _jsx("p", { className: css.bannerHint, children: t('status.loopbackRequiredHint') })] })) : state.kind === 'unreachable' ? (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('status.unreachable') }), _jsx("p", { className: css.bannerHint, children: t('status.unreachableHint') })] })) : (_jsxs(_Fragment, { children: [state.posture !== undefined && state.posture.hosts.some(host => host.exposed) && (_jsxs("div", { className: css.banner, role: "alert", children: [_jsx("p", { className: css.bannerTitle, children: t('posture.exposed') }), _jsx("p", { className: css.bannerHint, children: t('posture.exposedHint', { hosts: state.posture.hosts.filter(host => host.exposed).map(host => host.host).join(', ') }) })] })), _jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.cardHeader, children: [_jsx("span", { className: css.cardTitle, children: t('card.title') }), _jsxs("span", { className: css.badges, children: [state.public && _jsx("span", { className: clsx(css.badge, css.badgePublic), children: t('public.badge') }), _jsx("span", { className: clsx(css.badge, css[`badge-${statusOf(t, state).tone}`]), children: statusOf(t, state).text })] })] }), _jsx("div", { className: css.qrWrap, "data-testid": "remote-qr", children: _jsx(QRCodeSVG, { value: state.url, size: 184, level: "M", marginSize: 1, className: css.qr }) }), state.expired
31
31
  ? _jsx("p", { className: css.expired, children: t('pair.expired') })
32
- : _jsx("p", { className: css.expiry, children: t('pair.expires', { time: formatClock(state.expiresAt) }) })] }), _jsx("p", { className: css.hint, children: state.public ? t('pair.publicHint') : t('pair.hint') }), _jsx("div", { className: css.pairLinks, children: _jsxs("div", { className: css.pairLinkRow, children: [_jsxs("div", { className: css.pairLinkText, children: [_jsx("span", { className: css.pairLinkLabel, children: t('pair.linkLabel') }), _jsx("code", { className: css.link, title: state.url, children: state.url })] }), _jsxs("button", { type: "button", className: css.copyLink, onClick: () => onCopy(state.url), children: [_jsx(IconCopyOutline16, { size: 14 }), copied ? t('action.copied') : t('action.copyLink')] })] }) }), _jsx("p", { className: css.oneTimeHint, children: t('pair.oneTimeHint') }), state.phase === 'stopped' && _jsx("p", { className: css.stoppedHint, children: t('stopped.hint') }), state.tunnel !== undefined && state.tunnel.state !== 'running' && (_jsx("p", { className: state.tunnel.state === 'failed' ? css.tunnelFailed : css.tunnelNote, role: "status", children: state.tunnel.state === 'failed'
32
+ : _jsx("p", { className: css.expiry, children: t('pair.expires', { time: formatClock(state.expiresAt) }) })] }), _jsx("p", { className: css.hint, children: state.public ? t('pair.publicHint') : t('pair.hint') }), _jsxs("div", { className: css.pairLinks, children: [_jsxs("div", { className: css.pairLinkRow, children: [_jsxs("div", { className: css.pairLinkText, children: [_jsx("span", { className: css.pairLinkLabel, children: t('pair.linkLabel') }), _jsx("code", { className: css.link, title: state.url, children: state.url })] }), _jsxs("button", { type: "button", className: css.copyLink, onClick: () => onCopy(state.url), children: [_jsx(IconCopyOutline16, { size: 14 }), copied ? t('action.copied') : t('action.copyLink')] })] }), state.token !== undefined && state.token !== '' && (_jsxs("div", { className: css.pairLinkRow, children: [_jsxs("div", { className: css.pairLinkText, children: [_jsx("span", { className: css.pairLinkLabel, children: t('pair.tokenLabel') }), _jsx("code", { className: css.link, title: state.token, children: state.token })] }), _jsxs("button", { type: "button", className: css.copyLink, onClick: () => onCopyToken ? onCopyToken(state.token) : onCopy(state.token), children: [_jsx(IconCopyOutline16, { size: 14 }), copiedToken ? t('action.copiedToken') : t('action.copyToken')] })] }))] }), _jsxs("p", { className: css.oneTimeHint, children: [t('pair.oneTimeHint'), " ", t('pair.dockerHint')] }), state.phase === 'stopped' && _jsx("p", { className: css.stoppedHint, children: t('stopped.hint') }), state.tunnel !== undefined && state.tunnel.state !== 'running' && (_jsx("p", { className: state.tunnel.state === 'failed' ? css.tunnelFailed : css.tunnelNote, role: "status", children: state.tunnel.state === 'failed'
33
33
  ? t('tunnel.failed', { error: state.tunnel.error ?? t('tunnel.unknownError') })
34
34
  : t('tunnel.starting') })), state.relay?.state === 'registering' && (_jsx("p", { className: css.tunnelNote, role: "status", children: t('relay.registering') })), state.relay?.state === 'failed' && (_jsx("p", { className: css.tunnelFailed, role: "status", children: t('relay.failed', { error: state.relay.error ?? t('tunnel.unknownError') }) })), (state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (_jsxs("fieldset", { className: css.addresses, children: [_jsx("legend", { children: t('address.label') }), state.publicBaseUrl !== undefined && (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": t('address.public'), checked: state.public, onChange: onPickPublic }), _jsx("span", { children: t('address.public') }), _jsx("code", { className: css.addressValue, children: state.publicBaseUrl })] }, "public")), state.lanAddresses.map(address => (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": address, checked: !state.public && address === state.address, onChange: () => onPickAddress(address) }), _jsx("span", { children: t('address.lan') }), _jsx("code", { className: css.addressValue, children: address })] }, address))), _jsx("p", { className: css.addressHint, children: t('address.hint') })] })), _jsxs("div", { className: css.actions, children: [_jsxs("button", { type: "button", className: css.action, onClick: onStop, children: [_jsx(IconStopFill16, { size: 14 }), t('action.stop')] }), _jsxs("button", { type: "button", className: css.action, onClick: onRefresh, children: [_jsx(IconRefreshOutline16, { size: 14 }), t('action.refresh')] })] }), _jsxs("section", { className: css.devices, "data-testid": "remote-devices", "aria-label": t('devices.title'), children: [_jsx("h3", { className: css.devicesTitle, children: t('devices.title') }), state.devices.length === 0 ? (_jsx("p", { className: css.devicesEmpty, children: t('devices.empty') })) : (_jsx("ul", { className: css.deviceList, children: state.devices.map(device => (_jsxs("li", { className: css.deviceRow, children: [_jsxs("div", { className: css.deviceMeta, children: [_jsx("span", { className: css.deviceName, children: deviceNameFromUserAgent(device.userAgent) ?? t('devices.unknown') }), _jsx("span", { className: clsx(css.devicePresence, device.online ? css.deviceOnline : css.deviceOffline), children: device.online ? t('devices.online') : t('devices.offline') }), _jsx("span", { className: css.deviceSeen, children: t('devices.lastSeen', { time: formatLastSeen(device.lastSeenAt) }) })] }), _jsx("button", { type: "button", className: css.deviceRevoke, "aria-label": t('devices.revoke.label'), onClick: () => { onRevoke(device.id); }, children: t('devices.revoke') })] }, device.id))) }))] })] }))] }));
35
35
  }
@@ -28,6 +28,8 @@ export declare const zh: {
28
28
  'pair.expires': string;
29
29
  'pair.expired': string;
30
30
  'pair.linkLabel': string;
31
+ 'pair.tokenLabel': string;
32
+ 'pair.dockerHint': string;
31
33
  'pair.oneTimeHint': string;
32
34
  'pair.failed.title': string;
33
35
  'pair.failed.detail': string;
@@ -51,7 +53,9 @@ export declare const zh: {
51
53
  'action.refresh': string;
52
54
  'action.copy': string;
53
55
  'action.copyLink': string;
56
+ 'action.copyToken': string;
54
57
  'action.copied': string;
58
+ 'action.copiedToken': string;
55
59
  'devices.title': string;
56
60
  'devices.empty': string;
57
61
  'devices.unknown': string;
@@ -193,6 +197,8 @@ export declare const en: {
193
197
  'pair.expires': string;
194
198
  'pair.expired': string;
195
199
  'pair.linkLabel': string;
200
+ 'pair.tokenLabel': string;
201
+ 'pair.dockerHint': string;
196
202
  'pair.oneTimeHint': string;
197
203
  'pair.failed.title': string;
198
204
  'pair.failed.detail': string;
@@ -216,7 +222,9 @@ export declare const en: {
216
222
  'action.refresh': string;
217
223
  'action.copy': string;
218
224
  'action.copyLink': string;
225
+ 'action.copyToken': string;
219
226
  'action.copied': string;
227
+ 'action.copiedToken': string;
220
228
  'devices.title': string;
221
229
  'devices.empty': string;
222
230
  'devices.unknown': string;
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKsB,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqKmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqKsB,CAAA"}
@@ -28,6 +28,8 @@ export const zh = {
28
28
  'pair.expires': '二维码有效至 {time}',
29
29
  'pair.expired': '二维码已过期,请刷新',
30
30
  'pair.linkLabel': '配对链接',
31
+ 'pair.tokenLabel': '配对令牌',
32
+ 'pair.dockerHint': 'Docker 或反向代理环境下,可直接复制此令牌并在目标设备配对页面输入。',
31
33
  'pair.oneTimeHint': '链接含一次性令牌;任一设备配对成功后立即失效,配对下一台设备请刷新二维码。',
32
34
  'pair.failed.title': '配对失败',
33
35
  'pair.failed.detail': '链接无效或已使用,请回到电脑端刷新二维码后重新扫码。',
@@ -51,7 +53,9 @@ export const zh = {
51
53
  'action.refresh': '刷新二维码',
52
54
  'action.copy': '复制链接',
53
55
  'action.copyLink': '复制链接',
56
+ 'action.copyToken': '复制令牌',
54
57
  'action.copied': '已复制',
58
+ 'action.copiedToken': '已复制令牌',
55
59
  'devices.title': '已授权设备',
56
60
  'devices.empty': '还没有已配对的设备。扫码或打开链接后会出现在这里。',
57
61
  'devices.unknown': '未知设备',
@@ -191,6 +195,8 @@ export const en = {
191
195
  'pair.expires': 'QR code valid until {time}',
192
196
  'pair.expired': 'QR code expired — refresh it',
193
197
  'pair.linkLabel': 'Pairing link',
198
+ 'pair.tokenLabel': 'Pairing token',
199
+ 'pair.dockerHint': 'In Docker or reverse proxy environments, copy this token to pair directly on the target device.',
194
200
  'pair.oneTimeHint': 'The link carries one single-use token; it dies as soon as any device pairs. Refresh the QR to pair the next device.',
195
201
  'pair.failed.title': 'Pairing failed',
196
202
  'pair.failed.detail': 'The link is invalid or was already used. Refresh the QR code on your computer and scan again.',
@@ -214,7 +220,9 @@ export const en = {
214
220
  'action.refresh': 'Refresh QR',
215
221
  'action.copy': 'Copy link',
216
222
  'action.copyLink': 'Copy link',
223
+ 'action.copyToken': 'Copy token',
217
224
  'action.copied': 'Copied',
225
+ 'action.copiedToken': 'Token copied',
218
226
  'devices.title': 'Authorized devices',
219
227
  'devices.empty': 'No paired devices yet. Scan the QR or open the link to add one.',
220
228
  'devices.unknown': 'Unknown device',
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AAgOD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAyqBvC"}
1
+ {"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AA+ND;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAqqBvC"}