@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.
- package/README.i18n.yaml +2 -2
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +55 -21
- package/lib/client.js.map +1 -1
- package/lib/index.js +174 -39
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +11 -1
- package/lib/types/client/RemotePanel.d.ts +5 -1
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +8 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +7 -12
- package/lib/types/client/remote-channel.d.ts +0 -2
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +0 -4
- package/lib/types/index.d.ts +9 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +54 -27
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +16 -0
- package/lib/types/remote-api.d.ts +1 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +1 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +0 -1
- package/lib/types/remote-channel-rules.d.ts +0 -1
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +0 -1
- package/lib/types/remote-methods.d.ts +1 -4
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -5
- package/lib/types/routes.d.ts +26 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +150 -5
- package/package.json +18 -18
- package/src/client/RemoteEntry.tsx +12 -0
- package/src/client/RemotePanel.tsx +37 -2
- package/src/client/locales.ts +8 -0
- package/src/client/mobile-adapt.ts +5 -10
- package/src/client/remote-channel.ts +0 -3
- package/src/index.ts +59 -26
- package/src/loopback-proxy.ts +15 -0
- package/src/remote-api.ts +0 -1
- package/src/remote-channel-boot.ts +0 -1
- package/src/remote-channel-rules.ts +0 -2
- package/src/remote-methods.ts +1 -6
- 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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
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
|
-
|
|
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,+
|
|
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
|
-
|
|
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,
|
|
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') }),
|
|
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
|
|
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;
|
|
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"}
|