@linxin666/dsh-remote-web-ui 0.3.14 → 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 +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +55 -17
- package/lib/client.js.map +1 -1
- package/lib/index.js +171 -31
- 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/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/routes.d.ts +26 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +150 -5
- package/package.json +1 -1
- 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/index.ts +59 -26
- package/src/loopback-proxy.ts +15 -0
- 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
|
/**
|
|
@@ -3710,6 +3830,7 @@ const Config = z.object({
|
|
|
3710
3830
|
cookieName: z.string().min(1).default("dsh_pair"),
|
|
3711
3831
|
requirePairingForLan: z.boolean().default(true),
|
|
3712
3832
|
publicBaseUrl: z.string(),
|
|
3833
|
+
trustedHosts: z.array(z.string()),
|
|
3713
3834
|
devicesFile: z.string(),
|
|
3714
3835
|
autoTunnel: z.boolean().default(false),
|
|
3715
3836
|
tunnelToken: z.string().role("secret"),
|
|
@@ -3749,6 +3870,7 @@ const DEFAULTS = {
|
|
|
3749
3870
|
cookieName: "dsh_pair",
|
|
3750
3871
|
requirePairingForLan: true,
|
|
3751
3872
|
publicBaseUrl: void 0,
|
|
3873
|
+
trustedHosts: void 0,
|
|
3752
3874
|
devicesFile: defaultDevicesFile(),
|
|
3753
3875
|
autoTunnel: false,
|
|
3754
3876
|
tunnelToken: void 0,
|
|
@@ -3764,6 +3886,7 @@ const DEFAULTS = {
|
|
|
3764
3886
|
*/
|
|
3765
3887
|
const apply = mountOnce("@linxin666/dsh-remote-web-ui", applyImpl);
|
|
3766
3888
|
function applyImpl(ctx, config) {
|
|
3889
|
+
const envPublicBase = process.env.DSH_REMOTE_PUBLIC_BASE_URL?.trim() || void 0;
|
|
3767
3890
|
const resolved = {
|
|
3768
3891
|
tokenTtlMs: config?.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
3769
3892
|
offlineAfterMs: config?.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
|
@@ -3771,7 +3894,8 @@ function applyImpl(ctx, config) {
|
|
|
3771
3894
|
idleExpireMs: config?.idleExpireMs ?? DEFAULTS.idleExpireMs,
|
|
3772
3895
|
cookieName: config?.cookieName ?? DEFAULTS.cookieName,
|
|
3773
3896
|
requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
3774
|
-
publicBaseUrl: config?.publicBaseUrl,
|
|
3897
|
+
publicBaseUrl: config?.publicBaseUrl ?? envPublicBase,
|
|
3898
|
+
trustedHosts: config?.trustedHosts,
|
|
3775
3899
|
devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
|
|
3776
3900
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
3777
3901
|
tunnelToken: config?.tunnelToken,
|
|
@@ -3790,7 +3914,8 @@ function applyImpl(ctx, config) {
|
|
|
3790
3914
|
idleExpireMs: value.idleExpireMs ?? DEFAULTS.idleExpireMs,
|
|
3791
3915
|
cookieName: value.cookieName ?? DEFAULTS.cookieName,
|
|
3792
3916
|
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
3793
|
-
publicBaseUrl: value.publicBaseUrl,
|
|
3917
|
+
publicBaseUrl: value.publicBaseUrl ?? envPublicBase,
|
|
3918
|
+
trustedHosts: value.trustedHosts,
|
|
3794
3919
|
devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
|
|
3795
3920
|
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
3796
3921
|
tunnelToken: value.tunnelToken,
|
|
@@ -3880,13 +4005,15 @@ function applyImpl(ctx, config) {
|
|
|
3880
4005
|
let disposeRoutes;
|
|
3881
4006
|
let disposeSweep;
|
|
3882
4007
|
const requireFromHost = createRequire(import.meta.url);
|
|
3883
|
-
|
|
4008
|
+
/** Host-process resolve that degrades to "not installed" (undefined) instead of throwing. */
|
|
4009
|
+
const hostResolve = (specifier) => {
|
|
3884
4010
|
try {
|
|
3885
4011
|
return requireFromHost.resolve(specifier);
|
|
3886
4012
|
} catch {
|
|
3887
4013
|
return;
|
|
3888
4014
|
}
|
|
3889
|
-
}
|
|
4015
|
+
};
|
|
4016
|
+
const resolveAnchorPath = () => resolveAnchorManifest(hostResolve);
|
|
3890
4017
|
const releaseNotesCache = /* @__PURE__ */ new Map();
|
|
3891
4018
|
const fetchReleaseNotesCached = async (version) => {
|
|
3892
4019
|
const cached = releaseNotesCache.get(version);
|
|
@@ -3902,13 +4029,7 @@ function applyImpl(ctx, config) {
|
|
|
3902
4029
|
fence: (request) => isTrustedApiRequest(request, []),
|
|
3903
4030
|
check: () => checkUpdates({
|
|
3904
4031
|
anchorManifestPath: resolveAnchorPath(),
|
|
3905
|
-
resolve:
|
|
3906
|
-
try {
|
|
3907
|
-
return requireFromHost.resolve(specifier);
|
|
3908
|
-
} catch {
|
|
3909
|
-
return;
|
|
3910
|
-
}
|
|
3911
|
-
},
|
|
4032
|
+
resolve: hostResolve,
|
|
3912
4033
|
fetchLatest: (name) => fetchLatestVersion(name, fetch),
|
|
3913
4034
|
fetchReleaseNotes: fetchReleaseNotesCached
|
|
3914
4035
|
}),
|
|
@@ -3931,13 +4052,7 @@ function applyImpl(ctx, config) {
|
|
|
3931
4052
|
},
|
|
3932
4053
|
check: {
|
|
3933
4054
|
anchorManifestPath: resolveAnchorPath(),
|
|
3934
|
-
resolve:
|
|
3935
|
-
try {
|
|
3936
|
-
return requireFromHost.resolve(specifier);
|
|
3937
|
-
} catch {
|
|
3938
|
-
return;
|
|
3939
|
-
}
|
|
3940
|
-
},
|
|
4055
|
+
resolve: hostResolve,
|
|
3941
4056
|
fetchLatest: (name) => fetchLatestVersion(name, fetch),
|
|
3942
4057
|
fetchReleaseNotes: fetchReleaseNotesCached
|
|
3943
4058
|
}
|
|
@@ -4004,10 +4119,25 @@ function applyImpl(ctx, config) {
|
|
|
4004
4119
|
const routes = [
|
|
4005
4120
|
...makeRoutes({
|
|
4006
4121
|
service,
|
|
4007
|
-
lanAddresses: service.lanAddresses,
|
|
4008
4122
|
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
4009
4123
|
lanBindStatus,
|
|
4010
|
-
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
|
+
}
|
|
4011
4141
|
}),
|
|
4012
4142
|
...makeRemoteApiRoutes({
|
|
4013
4143
|
service,
|
|
@@ -4186,13 +4316,23 @@ function applyImpl(ctx, config) {
|
|
|
4186
4316
|
});
|
|
4187
4317
|
}), "remote-web-ui: remote channel boot patch");
|
|
4188
4318
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
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
|
+
});
|
|
4192
4333
|
sync();
|
|
4193
|
-
}
|
|
4194
|
-
|
|
4195
|
-
});
|
|
4334
|
+
}
|
|
4335
|
+
} catch {}
|
|
4196
4336
|
});
|
|
4197
4337
|
sync();
|
|
4198
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"}
|
|
@@ -214,7 +214,6 @@ const ADAPT_CSS = [
|
|
|
214
214
|
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"task-board\"],`,
|
|
215
215
|
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"git-graph\"],`,
|
|
216
216
|
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"pet\"],`,
|
|
217
|
-
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"perf\"],`,
|
|
218
217
|
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"usage\"]{display:none !important}`,
|
|
219
218
|
// The official workbench (Files / source control) mounts into a
|
|
220
219
|
// full-viewport portal layer with no data-dsh-plugin root, so the plugin
|
|
@@ -965,17 +964,13 @@ export function startMobileAdapt() {
|
|
|
965
964
|
}
|
|
966
965
|
lanOrigFocus.call(this, options);
|
|
967
966
|
};
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if (t instanceof Element && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') && t.closest('[class$="_composerSeat"]') !== null)
|
|
976
|
-
lastComposerTap = Date.now();
|
|
977
|
-
}, true);
|
|
978
|
-
}
|
|
967
|
+
document.addEventListener('pointerdown', (e) => {
|
|
968
|
+
if (!active)
|
|
969
|
+
return;
|
|
970
|
+
const t = e.target;
|
|
971
|
+
if (t instanceof Element && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') && t.closest('[class$="_composerSeat"]') !== null)
|
|
972
|
+
lastComposerTap = Date.now();
|
|
973
|
+
}, true);
|
|
979
974
|
// The plugin apply() wires toggleSidebar/closeDetails to ctx.layout once
|
|
980
975
|
// it is live, and flips the master switch when the settings snapshot
|
|
981
976
|
// settles.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -76,6 +76,13 @@ export interface Config {
|
|
|
76
76
|
* warning (LAN-only behavior preserved). Ignored while `autoTunnel` is on.
|
|
77
77
|
*/
|
|
78
78
|
publicBaseUrl?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Extra trusted host authorities (exact host:port or port-less host) allowed
|
|
81
|
+
* to access pairing endpoints and remote web services. Useful in Docker, reverse proxy,
|
|
82
|
+
* or NAT environments where incoming request Host headers differ from the container's
|
|
83
|
+
* internal network addresses. Can also be set via the DSH_REMOTE_TRUSTED_HOSTS env var.
|
|
84
|
+
*/
|
|
85
|
+
trustedHosts?: string[];
|
|
79
86
|
/**
|
|
80
87
|
* Absolute path to a JSON file where paired device sessions are persisted.
|
|
81
88
|
* Defaults to `$DSH_HOME/remote-web-ui-devices.json` so a paired device
|
|
@@ -146,8 +153,9 @@ export declare const Config: z<Config>;
|
|
|
146
153
|
* which legitimately resolves to `undefined` when unset (the schema keeps it
|
|
147
154
|
* optional, so `Required` alone would over-narrow it to `string`).
|
|
148
155
|
*/
|
|
149
|
-
type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'devicesFile' | 'lanBind' | 'profile' | 'tunnelToken'>> & {
|
|
156
|
+
type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'trustedHosts' | 'devicesFile' | 'lanBind' | 'profile' | 'tunnelToken'>> & {
|
|
150
157
|
publicBaseUrl: string | undefined;
|
|
158
|
+
trustedHosts: string[] | undefined;
|
|
151
159
|
devicesFile: string;
|
|
152
160
|
/** undefined until the user flips the toggle once; undefined never writes the patch. */
|
|
153
161
|
lanBind: boolean | undefined;
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,CAAC,MAAM,aAAa,CAAA;AAO3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAgCzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;;;;;;;WAWG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA+C,CAAA;AAElE;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,EAAsB,iBAAiB,CAAA;AAEpF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;;;;;OAWG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|