@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +56 -26
- package/README.zh.md +20 -15
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +64 -0
- package/lib/client.js +272 -47
- package/lib/client.js.map +1 -1
- package/lib/index.js +316 -60
- package/lib/mobile.js +487 -56
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +28 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +147 -6
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +13 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +36 -3
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +17 -4
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +30 -13
- package/lib/types/mobile/PairRequiredView.d.ts +7 -0
- package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.js +28 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.test.js +24 -0
- package/lib/types/mobile/api.d.ts +10 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +4 -0
- package/lib/types/mobile/index.js +14 -1
- package/lib/types/mobile/messages.d.ts +24 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +60 -23
- package/lib/types/mobile/messages.test.js +49 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +86 -0
- package/lib/types/mobile/pairing.d.ts +31 -0
- package/lib/types/mobile/pairing.d.ts.map +1 -0
- package/lib/types/mobile/pairing.js +62 -0
- package/lib/types/mobile/views/App.d.ts +12 -5
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +34 -7
- package/lib/types/mobile/views/App.test.d.ts +2 -0
- package/lib/types/mobile/views/App.test.d.ts.map +1 -0
- package/lib/types/mobile/views/App.test.js +52 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +34 -11
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +30 -4
- package/lib/types/mobile/views/SessionListView.test.js +58 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +4 -2
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +10 -1
- package/lib/types/mobile-pwa.d.ts +6 -0
- package/lib/types/mobile-pwa.d.ts.map +1 -0
- package/lib/types/mobile-pwa.js +17 -0
- package/lib/types/mobile-routes.d.ts +5 -8
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +115 -30
- package/lib/types/pairing-access.d.ts +28 -0
- package/lib/types/pairing-access.d.ts.map +1 -0
- package/lib/types/pairing-access.js +26 -0
- package/lib/types/pairing.d.ts +27 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +68 -0
- package/lib/types/pairing.persist.test.d.ts +2 -0
- package/lib/types/pairing.persist.test.d.ts.map +1 -0
- package/lib/types/pairing.persist.test.js +128 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +23 -3
- package/lib/types/tunnel.d.ts +1 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -2
- package/lib/types/update.d.ts +4 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +35 -12
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/RemoteEntry.tsx +12 -2
- package/src/client/RemoteSettingsCard.tsx +3 -2
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/UpdatePanel.tsx +2 -0
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +4 -0
- package/src/client/remote.module.css +26 -0
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +17 -3
- package/src/index.ts +46 -15
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +86 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -6
- package/src/mobile/views/ChatView.tsx +33 -10
- package/src/mobile/views/SessionListView.test.tsx +69 -1
- package/src/mobile/views/SessionListView.tsx +58 -5
- package/src/mobile/views/WorkspaceView.tsx +4 -2
- package/src/mobile-api.ts +9 -1
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +123 -30
- package/src/pairing-access.ts +36 -0
- package/src/pairing.persist.test.ts +145 -0
- package/src/pairing.ts +70 -1
- package/src/routes.ts +23 -3
- package/src/tunnel.ts +7 -2
- package/src/update.ts +42 -14
package/lib/index.js
CHANGED
|
@@ -3,16 +3,16 @@ import { setInterval as setInterval$1 } from "node:timers";
|
|
|
3
3
|
import { Service } from "@deepseek-ai/cordis";
|
|
4
4
|
import z from "schemastery";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
6
8
|
import { z as z$1 } from "zod";
|
|
7
9
|
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { RpcId } from "@deepseek-ai/dsh-host-apiproxy/api/rpc";
|
|
11
12
|
import { networkInterfaces } from "node:os";
|
|
12
13
|
import { Tunnel, bin, install } from "cloudflared";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-settings@0.1.0-rc.
|
|
15
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-settings@0.1.0-rc.7_@deepseek-ai+cordis@4.0.1_@deepseek-ai+dsh-brand@0_6b0eb868fb92d854799855c05c5fa43c/node_modules/@deepseek-ai/dsh-settings/lib/index.js
|
|
16
16
|
/**
|
|
17
17
|
* Structural secret redaction for settings values. `role('secret')` fields are
|
|
18
18
|
* removed from a value before it crosses a wire boundary; a sidecar records
|
|
@@ -271,6 +271,61 @@ var PairingService = class {
|
|
|
271
271
|
constructor(config, clock = defaultClock) {
|
|
272
272
|
this.config = config;
|
|
273
273
|
this.clock = clock;
|
|
274
|
+
this.loadPersisted();
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Restore device sessions persisted by a previous process run. A corrupt
|
|
278
|
+
* or missing file is tolerated (an empty device table, never a throw) —
|
|
279
|
+
* persistence is an availability convenience, not a security boundary.
|
|
280
|
+
*/
|
|
281
|
+
loadPersisted() {
|
|
282
|
+
const file = this.config.devicesFile;
|
|
283
|
+
if (file === void 0) return;
|
|
284
|
+
try {
|
|
285
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
286
|
+
if (typeof saved !== "object" || saved === null) return;
|
|
287
|
+
for (const [deviceId, session] of Object.entries(saved)) {
|
|
288
|
+
if (typeof deviceId !== "string") continue;
|
|
289
|
+
if (typeof session !== "object" || session === null) continue;
|
|
290
|
+
const { createdAt, lastSeenAt } = session;
|
|
291
|
+
if (typeof createdAt !== "number" || typeof lastSeenAt !== "number") continue;
|
|
292
|
+
this.devices.set(deviceId, {
|
|
293
|
+
createdAt,
|
|
294
|
+
lastSeenAt
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
this.clampToMaxDevices();
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
/** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
|
|
301
|
+
clampToMaxDevices() {
|
|
302
|
+
if (this.devices.size <= this.config.maxDevices) return;
|
|
303
|
+
const overflow = this.devices.size - this.config.maxDevices;
|
|
304
|
+
const ordered = [...this.devices.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
305
|
+
for (const [id] of ordered.slice(0, overflow)) this.devices.delete(id);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Write the current device table to the configured file. Called on the
|
|
309
|
+
* mutation boundaries that change the set of live sessions (accept and
|
|
310
|
+
* stop); `lastSeenAt` refreshes are deliberately not persisted here so a
|
|
311
|
+
* per-request write storm is avoided — after a restart the phone's first
|
|
312
|
+
* heartbeat re-warms its own session.
|
|
313
|
+
*
|
|
314
|
+
* Device ids are session credentials (the gate authorizes requests by the
|
|
315
|
+
* cookie's device id), so the file is written 0600 via a temp file and
|
|
316
|
+
* atomic rename; a crash mid-write can never leave a half-written store.
|
|
317
|
+
*/
|
|
318
|
+
persist() {
|
|
319
|
+
const file = this.config.devicesFile;
|
|
320
|
+
if (file === void 0) return;
|
|
321
|
+
try {
|
|
322
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
323
|
+
const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
324
|
+
writeFileSync(temp, JSON.stringify(Object.fromEntries(this.devices)), { mode: 384 });
|
|
325
|
+
renameSync(temp, file);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error("remote-web-ui: failed to persist paired devices", error);
|
|
328
|
+
}
|
|
274
329
|
}
|
|
275
330
|
/** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
|
|
276
331
|
get lanBaseUrl() {
|
|
@@ -365,6 +420,7 @@ var PairingService = class {
|
|
|
365
420
|
createdAt: now,
|
|
366
421
|
lastSeenAt: now
|
|
367
422
|
});
|
|
423
|
+
this.persist();
|
|
368
424
|
this.notify();
|
|
369
425
|
return {
|
|
370
426
|
ok: true,
|
|
@@ -379,6 +435,7 @@ var PairingService = class {
|
|
|
379
435
|
stop() {
|
|
380
436
|
this.tokens.clear();
|
|
381
437
|
this.devices.clear();
|
|
438
|
+
this.persist();
|
|
382
439
|
this.stopped = true;
|
|
383
440
|
this.notify();
|
|
384
441
|
}
|
|
@@ -566,11 +623,49 @@ function makeGateListener(service, requirePairingForLan = true, enabled = true)
|
|
|
566
623
|
if (isLoopbackClient(request)) return next();
|
|
567
624
|
if (!(typeof enabled === "function" ? enabled() : enabled)) return false;
|
|
568
625
|
if (!(typeof requirePairingForLan === "function" ? requirePairingForLan() : requirePairingForLan)) return next();
|
|
569
|
-
|
|
570
|
-
if (deviceId === void 0) return false;
|
|
571
|
-
return service.touchDevice(deviceId) ? next() : false;
|
|
626
|
+
return isPairedDeviceRequest(service, request) ? next() : false;
|
|
572
627
|
};
|
|
573
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Whether a request carries a live, non-revoked paired-device cookie for
|
|
631
|
+
* this service. Sibling host routes outside /api (aionui-panel, etc.) use
|
|
632
|
+
* the same check via the remoteWebUiPairing service.
|
|
633
|
+
* @param service - the pairing service that owns the device table.
|
|
634
|
+
* @param request - the incoming HTTP request.
|
|
635
|
+
* @returns true when the cookie names a live session (and lastSeenAt was refreshed).
|
|
636
|
+
*/
|
|
637
|
+
function isPairedDeviceRequest(service, request) {
|
|
638
|
+
const deviceId = readCookie(request.headers.cookie, service.config.cookieName);
|
|
639
|
+
if (deviceId === void 0) return false;
|
|
640
|
+
return service.touchDevice(deviceId);
|
|
641
|
+
}
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/pairing-access.ts
|
|
644
|
+
/** Named lookup key sibling plugins pass to ctx.get. */
|
|
645
|
+
const REMOTE_WEB_UI_PAIRING = "remoteWebUiPairing";
|
|
646
|
+
/**
|
|
647
|
+
* Pairing identity for one HTTP request. Structural: consumers must not
|
|
648
|
+
* import this class, only the method shape.
|
|
649
|
+
*/
|
|
650
|
+
var RemoteWebUiPairing = class extends Service {
|
|
651
|
+
check;
|
|
652
|
+
/**
|
|
653
|
+
* @param ctx - host plugin context.
|
|
654
|
+
* @param check - live cookie + session predicate (re-read per request).
|
|
655
|
+
*/
|
|
656
|
+
constructor(ctx, check) {
|
|
657
|
+
super(ctx, REMOTE_WEB_UI_PAIRING);
|
|
658
|
+
this.check = check;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Whether the request carries a live paired-device cookie.
|
|
662
|
+
* @param request - the incoming HTTP request.
|
|
663
|
+
* @returns true when the session is live and was refreshed.
|
|
664
|
+
*/
|
|
665
|
+
isPairedDevice(request) {
|
|
666
|
+
return this.check(request);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
574
669
|
//#endregion
|
|
575
670
|
//#region src/http.ts
|
|
576
671
|
/** One JSON response. */
|
|
@@ -773,7 +868,9 @@ function makeRoutes(deps) {
|
|
|
773
868
|
const ACCEPT_MAX_ATTEMPTS = 10;
|
|
774
869
|
const ACCEPT_WINDOW_MS = 3e4;
|
|
775
870
|
const rateLimitAccept = (req) => {
|
|
776
|
-
const
|
|
871
|
+
const socketIp = req.socket?.remoteAddress ?? "unknown";
|
|
872
|
+
const forwarded = typeof req.headers["x-forwarded-for"] === "string" ? (req.headers["x-forwarded-for"].split(",")[0] ?? "").trim() : void 0;
|
|
873
|
+
const ip = forwarded === void 0 || forwarded === "" ? socketIp : socketIp + "|" + forwarded;
|
|
777
874
|
const nowMs = Date.now();
|
|
778
875
|
if (acceptAttempts.size > 256) {
|
|
779
876
|
for (const [key, attempt] of acceptAttempts) if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS) acceptAttempts.delete(key);
|
|
@@ -814,7 +911,7 @@ function makeRoutes(deps) {
|
|
|
814
911
|
if (base === void 0) throw new Error("remote-web-ui: base unavailable");
|
|
815
912
|
writeJson(res, 200, {
|
|
816
913
|
ok: true,
|
|
817
|
-
url: `${base}/?pair=${token}${workspaceId === void 0 ? "" : `&workspace=${encodeURIComponent(workspaceId)}`}`,
|
|
914
|
+
url: `${base}/m/?pair=${token}${workspaceId === void 0 ? "" : `&workspace=${encodeURIComponent(workspaceId)}`}`,
|
|
818
915
|
token,
|
|
819
916
|
expiresAt,
|
|
820
917
|
lanAddresses: service.lanAddresses,
|
|
@@ -927,10 +1024,16 @@ function makeRoutes(deps) {
|
|
|
927
1024
|
return;
|
|
928
1025
|
}
|
|
929
1026
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
1027
|
+
const paired = deviceId !== void 0 && service.hasDevice(deviceId);
|
|
1028
|
+
const snapshot = service.snapshot();
|
|
930
1029
|
writeJson(res, 200, {
|
|
931
1030
|
ok: true,
|
|
932
|
-
paired
|
|
933
|
-
...
|
|
1031
|
+
paired,
|
|
1032
|
+
...paired ? snapshot : {
|
|
1033
|
+
phase: snapshot.phase,
|
|
1034
|
+
lanAvailable: snapshot.lanAvailable,
|
|
1035
|
+
lanAddresses: snapshot.lanAddresses
|
|
1036
|
+
}
|
|
934
1037
|
});
|
|
935
1038
|
};
|
|
936
1039
|
const handleEvents = (req, res) => {
|
|
@@ -981,23 +1084,25 @@ function makeRoutes(deps) {
|
|
|
981
1084
|
//#endregion
|
|
982
1085
|
//#region src/mobile-routes.ts
|
|
983
1086
|
/**
|
|
984
|
-
* The mobile surface's page routes:
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
* already crosses the api/gate fence), so no host-side data plumbing is
|
|
989
|
-
* needed here — only static serving, loopback+paired-fence via the normal
|
|
990
|
-
* webserver route registration.
|
|
1087
|
+
* The mobile surface's page routes: /m canonicalizes to /m/, which serves the
|
|
1088
|
+
* standalone phone UI. The page and worker live in the /m/ scope so the PWA
|
|
1089
|
+
* can cache only its static shell; the paired-device data channel remains at
|
|
1090
|
+
* /m/api and is never handled by the worker.
|
|
991
1091
|
*/
|
|
1092
|
+
const MOBILE_ROOT = "/m/";
|
|
1093
|
+
const MOBILE_BUNDLE_URL = "/m/mobile.js";
|
|
1094
|
+
const MOBILE_MANIFEST_URL = "/m/manifest.webmanifest";
|
|
1095
|
+
const MOBILE_WORKER_URL = "/m/service-worker.js";
|
|
1096
|
+
const MOBILE_OFFLINE_URL = "/m/offline.html";
|
|
992
1097
|
/** The standalone mobile bundle (built artifact, next to this file's own lib output). */
|
|
993
1098
|
function mobileBundlePath() {
|
|
994
1099
|
return fileURLToPath(new URL("../lib/mobile.js", import.meta.url));
|
|
995
1100
|
}
|
|
996
|
-
/**
|
|
997
|
-
function
|
|
998
|
-
return fileURLToPath(new URL("../assets/
|
|
1101
|
+
/** A package asset available to the host after a registry or git install. */
|
|
1102
|
+
function mobileAssetPath(name) {
|
|
1103
|
+
return fileURLToPath(new URL("../assets/" + name, import.meta.url));
|
|
999
1104
|
}
|
|
1000
|
-
/** The mobile page shell:
|
|
1105
|
+
/** The mobile page shell: self-contained, with no document-level user data. */
|
|
1001
1106
|
function pageHtml(bundleUrl) {
|
|
1002
1107
|
return [
|
|
1003
1108
|
"<!doctype html>",
|
|
@@ -1007,26 +1112,80 @@ function pageHtml(bundleUrl) {
|
|
|
1007
1112
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\">",
|
|
1008
1113
|
"<meta name=\"theme-color\" content=\"#f3f5f9\">",
|
|
1009
1114
|
"<meta name=\"referrer\" content=\"no-referrer\">",
|
|
1115
|
+
"<meta name=\"mobile-web-app-capable\" content=\"yes\">",
|
|
1116
|
+
"<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">",
|
|
1117
|
+
"<meta name=\"apple-mobile-web-app-title\" content=\"DSH Remote\">",
|
|
1118
|
+
"<link rel=\"manifest\" href=\"/m/manifest.webmanifest\">",
|
|
1010
1119
|
"<link rel=\"apple-touch-icon\" href=\"/m/apple-touch-icon.png\">",
|
|
1011
1120
|
"<title>移动端远程控制</title>",
|
|
1012
1121
|
"</head>",
|
|
1013
1122
|
"<body>",
|
|
1014
1123
|
"<div id=\"root\"></div>",
|
|
1015
|
-
|
|
1124
|
+
"<script type=\"module\" src=\"" + bundleUrl + "\"><\/script>",
|
|
1016
1125
|
"</body>",
|
|
1017
1126
|
"</html>"
|
|
1018
1127
|
].join("");
|
|
1019
1128
|
}
|
|
1020
|
-
/**
|
|
1129
|
+
/** The installable app identity and icon declarations for the /m scope. */
|
|
1130
|
+
function manifestJson() {
|
|
1131
|
+
return JSON.stringify({
|
|
1132
|
+
id: MOBILE_ROOT,
|
|
1133
|
+
name: "DSH Remote",
|
|
1134
|
+
short_name: "DSH Remote",
|
|
1135
|
+
start_url: MOBILE_ROOT,
|
|
1136
|
+
scope: MOBILE_ROOT,
|
|
1137
|
+
display: "standalone",
|
|
1138
|
+
background_color: "#151424",
|
|
1139
|
+
theme_color: "#f3f5f9",
|
|
1140
|
+
icons: [{
|
|
1141
|
+
src: "/m/icon-192.png",
|
|
1142
|
+
sizes: "192x192",
|
|
1143
|
+
type: "image/png",
|
|
1144
|
+
purpose: "any"
|
|
1145
|
+
}, {
|
|
1146
|
+
src: "/m/icon-512.png",
|
|
1147
|
+
sizes: "512x512",
|
|
1148
|
+
type: "image/png",
|
|
1149
|
+
purpose: "any maskable"
|
|
1150
|
+
}]
|
|
1151
|
+
}, null, 2);
|
|
1152
|
+
}
|
|
1153
|
+
/** A static fallback that contains no paired-device, session, or workspace data. */
|
|
1154
|
+
function offlineHtml() {
|
|
1155
|
+
return [
|
|
1156
|
+
"<!doctype html>",
|
|
1157
|
+
"<html lang=\"zh-CN\">",
|
|
1158
|
+
"<head>",
|
|
1159
|
+
"<meta charset=\"utf-8\">",
|
|
1160
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">",
|
|
1161
|
+
"<meta name=\"theme-color\" content=\"#151424\">",
|
|
1162
|
+
"<title>DSH Remote</title>",
|
|
1163
|
+
"<style>body{margin:0;background:#151424;color:#f3f5f9;font:16px system-ui,sans-serif}main{box-sizing:border-box;display:grid;min-height:100vh;place-content:center;padding:32px;text-align:center}h1{margin:0 0 12px;font-size:24px}p{margin:0;color:#c4c7d8;line-height:1.5}</style>",
|
|
1164
|
+
"</head>",
|
|
1165
|
+
"<body><main><h1>DSH Remote</h1><p>Cannot reach the running DSH host. Restore the connection and reopen the app.</p></main></body>",
|
|
1166
|
+
"</html>"
|
|
1167
|
+
].join("");
|
|
1168
|
+
}
|
|
1169
|
+
/** Send a small static UTF-8 body with revalidation headers. */
|
|
1021
1170
|
function writeStatic(res, status, type, body) {
|
|
1022
1171
|
res.writeHead(status, {
|
|
1023
|
-
"content-type":
|
|
1172
|
+
"content-type": type + "; charset=utf-8",
|
|
1024
1173
|
"cache-control": "no-cache",
|
|
1025
1174
|
"referrer-policy": "no-referrer"
|
|
1026
1175
|
});
|
|
1027
1176
|
res.end(body);
|
|
1028
1177
|
}
|
|
1029
|
-
/** Send the
|
|
1178
|
+
/** Send the worker with the same scope it registers for, without HTTP caching. */
|
|
1179
|
+
function writeServiceWorker(res, body) {
|
|
1180
|
+
res.writeHead(200, {
|
|
1181
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1182
|
+
"cache-control": "no-cache",
|
|
1183
|
+
"referrer-policy": "no-referrer",
|
|
1184
|
+
"service-worker-allowed": MOBILE_ROOT
|
|
1185
|
+
});
|
|
1186
|
+
res.end(body);
|
|
1187
|
+
}
|
|
1188
|
+
/** Send a PNG body without decoding it to UTF-8. */
|
|
1030
1189
|
function writePng(res, status, body) {
|
|
1031
1190
|
res.writeHead(status, {
|
|
1032
1191
|
"content-type": "image/png",
|
|
@@ -1035,13 +1194,29 @@ function writePng(res, status, body) {
|
|
|
1035
1194
|
});
|
|
1036
1195
|
res.end(body);
|
|
1037
1196
|
}
|
|
1197
|
+
/** Canonicalize the old /m route while retaining pair and workspace query parameters. */
|
|
1198
|
+
function redirectToMobileRoot(req, res) {
|
|
1199
|
+
const url = new URL(req.url ?? "/m", "http://x");
|
|
1200
|
+
res.writeHead(308, {
|
|
1201
|
+
location: MOBILE_ROOT + url.search,
|
|
1202
|
+
"cache-control": "no-store",
|
|
1203
|
+
"referrer-policy": "no-referrer"
|
|
1204
|
+
});
|
|
1205
|
+
res.end();
|
|
1206
|
+
}
|
|
1038
1207
|
/**
|
|
1039
1208
|
* Build the mobile page routes.
|
|
1040
|
-
* @returns the
|
|
1209
|
+
* @returns Exact routes for the canonical page, static shell, and PWA assets.
|
|
1041
1210
|
*/
|
|
1042
1211
|
function makeMobileRoutes() {
|
|
1043
1212
|
const handlePage = (_req, res) => {
|
|
1044
|
-
writeStatic(res, 200, "text/html", pageHtml(
|
|
1213
|
+
writeStatic(res, 200, "text/html", pageHtml(MOBILE_BUNDLE_URL));
|
|
1214
|
+
};
|
|
1215
|
+
const handleManifest = (_req, res) => {
|
|
1216
|
+
writeStatic(res, 200, "application/manifest+json", manifestJson());
|
|
1217
|
+
};
|
|
1218
|
+
const handleOffline = (_req, res) => {
|
|
1219
|
+
writeStatic(res, 200, "text/html", offlineHtml());
|
|
1045
1220
|
};
|
|
1046
1221
|
let bundleBody;
|
|
1047
1222
|
const handleBundle = async (_req, res) => {
|
|
@@ -1060,41 +1235,95 @@ function makeMobileRoutes() {
|
|
|
1060
1235
|
}
|
|
1061
1236
|
writeStatic(res, 200, "text/javascript", bundleBody);
|
|
1062
1237
|
};
|
|
1063
|
-
|
|
1064
|
-
|
|
1238
|
+
let workerBody;
|
|
1239
|
+
const handleWorker = async (_req, res) => {
|
|
1240
|
+
if (workerBody === void 0) {
|
|
1241
|
+
const path = mobileAssetPath("mobile-service-worker.js");
|
|
1242
|
+
if (!existsSync(path)) {
|
|
1243
|
+
writeStatic(res, 503, "text/plain", "mobile service worker not found");
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
workerBody = await readFile(path, "utf8");
|
|
1248
|
+
} catch {
|
|
1249
|
+
writeStatic(res, 500, "text/plain", "failed to read the mobile service worker");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
writeServiceWorker(res, workerBody);
|
|
1254
|
+
};
|
|
1255
|
+
const makeIconHandler = (asset) => async (_req, res) => {
|
|
1256
|
+
const path = mobileAssetPath(asset);
|
|
1065
1257
|
if (!existsSync(path)) {
|
|
1066
|
-
writeStatic(res, 404, "text/plain", "
|
|
1258
|
+
writeStatic(res, 404, "text/plain", asset + " not found");
|
|
1067
1259
|
return;
|
|
1068
1260
|
}
|
|
1069
1261
|
try {
|
|
1070
1262
|
writePng(res, 200, await readFile(path));
|
|
1071
1263
|
} catch {
|
|
1072
|
-
writeStatic(res, 500, "text/plain", "failed to read
|
|
1264
|
+
writeStatic(res, 500, "text/plain", "failed to read " + asset);
|
|
1073
1265
|
}
|
|
1074
1266
|
};
|
|
1075
1267
|
return [
|
|
1076
1268
|
{
|
|
1077
1269
|
kind: "exact",
|
|
1078
1270
|
path: "/m",
|
|
1271
|
+
handler: redirectToMobileRoot
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
kind: "exact",
|
|
1275
|
+
path: MOBILE_ROOT,
|
|
1079
1276
|
handler: handlePage
|
|
1080
1277
|
},
|
|
1081
1278
|
{
|
|
1082
1279
|
kind: "exact",
|
|
1083
|
-
path:
|
|
1280
|
+
path: MOBILE_BUNDLE_URL,
|
|
1084
1281
|
handler: handleBundle
|
|
1085
1282
|
},
|
|
1283
|
+
{
|
|
1284
|
+
kind: "exact",
|
|
1285
|
+
path: MOBILE_MANIFEST_URL,
|
|
1286
|
+
handler: handleManifest
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
kind: "exact",
|
|
1290
|
+
path: MOBILE_WORKER_URL,
|
|
1291
|
+
handler: handleWorker
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
kind: "exact",
|
|
1295
|
+
path: MOBILE_OFFLINE_URL,
|
|
1296
|
+
handler: handleOffline
|
|
1297
|
+
},
|
|
1086
1298
|
{
|
|
1087
1299
|
kind: "exact",
|
|
1088
1300
|
path: "/m/apple-touch-icon.png",
|
|
1089
|
-
handler:
|
|
1301
|
+
handler: makeIconHandler("apple-touch-icon.png")
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
kind: "exact",
|
|
1305
|
+
path: "/m/icon-192.png",
|
|
1306
|
+
handler: makeIconHandler("mobile-icon-192.png")
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
kind: "exact",
|
|
1310
|
+
path: "/m/icon-512.png",
|
|
1311
|
+
handler: makeIconHandler("mobile-icon-512.png")
|
|
1090
1312
|
}
|
|
1091
1313
|
];
|
|
1092
1314
|
}
|
|
1093
1315
|
//#endregion
|
|
1094
1316
|
//#region src/mobile-api.ts
|
|
1095
|
-
/**
|
|
1317
|
+
/**
|
|
1318
|
+
* Methods the phone surface may call. Everything else is refused HERE — but
|
|
1319
|
+
* note the paired-device cookie also passes the global api/gate for the full
|
|
1320
|
+
* ApiProxy surface (gate.ts), so a paired phone is a full-control credential:
|
|
1321
|
+
* the allowlist only constrains this /m/api proxy, not the cookie's reach.
|
|
1322
|
+
* stop() revokes every device; there is no per-device revocation yet.
|
|
1323
|
+
*/
|
|
1096
1324
|
const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1097
1325
|
"workspace.list",
|
|
1326
|
+
"agentPreset.list",
|
|
1098
1327
|
"session.create",
|
|
1099
1328
|
"session.list",
|
|
1100
1329
|
"session.history",
|
|
@@ -1359,6 +1588,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
1359
1588
|
result: response.result
|
|
1360
1589
|
});
|
|
1361
1590
|
if (method === "workspace.list") return wrap(await apiProxy.workspace.list(request));
|
|
1591
|
+
if (method === "agentPreset.list") return wrap(await apiProxy.agentPresets.list(request));
|
|
1362
1592
|
if (method === "session.create") return wrap(await apiProxy.sessions.create(request));
|
|
1363
1593
|
if (method === "session.history") return wrap(await apiProxy.sessions.history(request));
|
|
1364
1594
|
if (method === "session.search") return wrap(await apiProxy.sessions.search(request, signal ?? new AbortController().signal));
|
|
@@ -1436,6 +1666,7 @@ var TunnelManager = class {
|
|
|
1436
1666
|
urlTimer;
|
|
1437
1667
|
restartTimer;
|
|
1438
1668
|
attempts = 0;
|
|
1669
|
+
generation = 0;
|
|
1439
1670
|
stopping = false;
|
|
1440
1671
|
urlListeners = /* @__PURE__ */ new Set();
|
|
1441
1672
|
phaseListeners = /* @__PURE__ */ new Set();
|
|
@@ -1470,6 +1701,7 @@ var TunnelManager = class {
|
|
|
1470
1701
|
this.stopping = false;
|
|
1471
1702
|
this.targetUrl = targetUrl;
|
|
1472
1703
|
this.attempts = 0;
|
|
1704
|
+
this.generation += 1;
|
|
1473
1705
|
this.attempt();
|
|
1474
1706
|
}
|
|
1475
1707
|
/** Stop the tunnel for good: no restarts, no state. */
|
|
@@ -1499,12 +1731,13 @@ var TunnelManager = class {
|
|
|
1499
1731
|
}
|
|
1500
1732
|
attempt() {
|
|
1501
1733
|
if (this.stopping || this.targetUrl === void 0) return;
|
|
1734
|
+
const gen = this.generation;
|
|
1502
1735
|
this.setPhase("starting");
|
|
1503
1736
|
this.handle = void 0;
|
|
1504
1737
|
this.url = void 0;
|
|
1505
1738
|
this.error = void 0;
|
|
1506
1739
|
this.ensureBinary().then(() => {
|
|
1507
|
-
if (this.stopping || this.targetUrl === void 0) return;
|
|
1740
|
+
if (this.stopping || this.targetUrl === void 0 || gen !== this.generation) return;
|
|
1508
1741
|
const handle = this.factory(this.targetUrl);
|
|
1509
1742
|
this.handle = handle;
|
|
1510
1743
|
this.urlTimer = this.timer.setTimeout(() => {
|
|
@@ -1523,7 +1756,7 @@ var TunnelManager = class {
|
|
|
1523
1756
|
this.error = value instanceof Error ? value.message : String(value);
|
|
1524
1757
|
});
|
|
1525
1758
|
}).catch((value) => {
|
|
1526
|
-
if (this.stopping || this.targetUrl === void 0) return;
|
|
1759
|
+
if (this.stopping || this.targetUrl === void 0 || gen !== this.generation) return;
|
|
1527
1760
|
const message = value instanceof Error ? value.message : String(value);
|
|
1528
1761
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
|
1529
1762
|
});
|
|
@@ -1715,12 +1948,13 @@ function resolveUpdateTarget(deps) {
|
|
|
1715
1948
|
if (anchor === void 0) return { error: "not-found" };
|
|
1716
1949
|
const profile = findProfile(manifestPath);
|
|
1717
1950
|
if (profile === void 0) return { error: "link" };
|
|
1718
|
-
const
|
|
1951
|
+
const profileManifest = readManifest(join(profile.dir, "package.json"));
|
|
1952
|
+
const spec = (profileManifest?.dependencies)?.[anchor];
|
|
1719
1953
|
if (isLinkedSpec(spec)) return { error: "link" };
|
|
1720
1954
|
return {
|
|
1721
1955
|
profileName: profile.name,
|
|
1722
1956
|
profileDir: profile.dir,
|
|
1723
|
-
packages:
|
|
1957
|
+
packages: familyUpdatePackages(anchor, manifest, profileManifest)
|
|
1724
1958
|
};
|
|
1725
1959
|
}
|
|
1726
1960
|
/** Family children of the anchor: its dependencies under the family scope. */
|
|
@@ -1731,6 +1965,14 @@ function familyChildren(anchorManifest) {
|
|
|
1731
1965
|
for (const [name, spec] of Object.entries(dependencies)) if (name.startsWith("@linxin666/") && typeof spec === "string") names.push(name);
|
|
1732
1966
|
return names;
|
|
1733
1967
|
}
|
|
1968
|
+
/** Registry-managed family packages covered by one update operation. */
|
|
1969
|
+
function familyUpdatePackages(anchor, anchorManifest, profileManifest) {
|
|
1970
|
+
const names = /* @__PURE__ */ new Set([anchor, ...familyChildren(anchorManifest)]);
|
|
1971
|
+
const dependencies = profileManifest?.dependencies;
|
|
1972
|
+
if (typeof dependencies !== "object" || dependencies === null) return [...names];
|
|
1973
|
+
for (const [name, spec] of Object.entries(dependencies)) if (name.startsWith("@linxin666/") && typeof spec === "string" && !isLinkedSpec(spec)) names.add(name);
|
|
1974
|
+
return [...names];
|
|
1975
|
+
}
|
|
1734
1976
|
/**
|
|
1735
1977
|
* Probe the npm registry for one package's latest release.
|
|
1736
1978
|
* @param name - the package name (scope slash URL-encoded).
|
|
@@ -1745,7 +1987,7 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
|
|
|
1745
1987
|
controller.abort();
|
|
1746
1988
|
}, timeoutMs);
|
|
1747
1989
|
try {
|
|
1748
|
-
const response = await fetchImpl("https://registry.npmjs.org/" + name.replace("/", "%2F") + "/latest");
|
|
1990
|
+
const response = await fetchImpl("https://registry.npmjs.org/" + name.replace("/", "%2F") + "/latest", { signal: controller.signal });
|
|
1749
1991
|
if (!response.ok) return void 0;
|
|
1750
1992
|
const body = await response.json();
|
|
1751
1993
|
if (typeof body !== "object" || body === null) return void 0;
|
|
@@ -1766,14 +2008,15 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
|
|
|
1766
2008
|
*/
|
|
1767
2009
|
const VERSION_UNKNOWN = "0.0.0";
|
|
1768
2010
|
/** The resolved current version of one family package (probe failure tolerated). */
|
|
1769
|
-
function readInstalledVersion(resolve, name) {
|
|
2011
|
+
function readInstalledVersion(resolve, name, profileDir) {
|
|
1770
2012
|
try {
|
|
1771
2013
|
const path = resolve(name + "/package.json");
|
|
1772
2014
|
const version = path === void 0 ? void 0 : readManifest(path)?.version;
|
|
1773
|
-
|
|
1774
|
-
} catch {
|
|
1775
|
-
|
|
1776
|
-
|
|
2015
|
+
if (typeof version === "string") return version;
|
|
2016
|
+
} catch {}
|
|
2017
|
+
if (profileDir === void 0 || !/^@linxin666\/[a-z0-9][a-z0-9._-]*$/.test(name)) return VERSION_UNKNOWN;
|
|
2018
|
+
const version = readManifest(join(profileDir, "node_modules", ...name.split("/"), "package.json"))?.version;
|
|
2019
|
+
return typeof version === "string" ? version : VERSION_UNKNOWN;
|
|
1777
2020
|
}
|
|
1778
2021
|
/**
|
|
1779
2022
|
* Build the update status: locate the anchor, detect the install mode, and
|
|
@@ -1808,14 +2051,14 @@ async function checkUpdates(deps) {
|
|
|
1808
2051
|
packages: [],
|
|
1809
2052
|
outdated: false
|
|
1810
2053
|
};
|
|
1811
|
-
const names =
|
|
2054
|
+
const names = familyUpdatePackages(anchor, manifest, profileManifest);
|
|
1812
2055
|
const latestList = await Promise.all(names.map((name) => deps.fetchLatest(name)));
|
|
1813
2056
|
const packages = [];
|
|
1814
2057
|
let probeFailures = 0;
|
|
1815
2058
|
names.forEach((name, index) => {
|
|
1816
2059
|
const latest = latestList[index];
|
|
1817
2060
|
if (latest === void 0) probeFailures++;
|
|
1818
|
-
const current = readInstalledVersion(deps.resolve, name);
|
|
2061
|
+
const current = readInstalledVersion(deps.resolve, name, profile.dir);
|
|
1819
2062
|
packages.push({
|
|
1820
2063
|
name,
|
|
1821
2064
|
current,
|
|
@@ -2027,7 +2270,7 @@ function runUpdate(deps) {
|
|
|
2027
2270
|
async function runUpdateVerified(deps) {
|
|
2028
2271
|
const before = /* @__PURE__ */ new Map();
|
|
2029
2272
|
for (const name of deps.run.packages) {
|
|
2030
|
-
const version = readInstalledVersion(deps.check.resolve, name);
|
|
2273
|
+
const version = readInstalledVersion(deps.check.resolve, name, deps.run.profileDir);
|
|
2031
2274
|
if (version !== VERSION_UNKNOWN) before.set(name, version);
|
|
2032
2275
|
}
|
|
2033
2276
|
const result = await runUpdate(deps.run);
|
|
@@ -2184,12 +2427,28 @@ const Config = z.object({
|
|
|
2184
2427
|
cookieName: z.string().min(1).default("dsh_pair"),
|
|
2185
2428
|
requirePairingForLan: z.boolean().default(true),
|
|
2186
2429
|
publicBaseUrl: z.string(),
|
|
2430
|
+
devicesFile: z.string(),
|
|
2187
2431
|
autoTunnel: z.boolean().default(false),
|
|
2188
2432
|
mobileEnterToSend: z.boolean().default(true),
|
|
2189
2433
|
enabled: z.boolean().default(true)
|
|
2190
2434
|
});
|
|
2191
2435
|
/** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
|
|
2192
2436
|
const SWEEP_INTERVAL_MS = 1e4;
|
|
2437
|
+
/**
|
|
2438
|
+
* The single mapping from resolved plugin config to the pairing service
|
|
2439
|
+
* config. Both the constructed service and every live settings sync reuse
|
|
2440
|
+
* it, so no field can be silently dropped when the web settings surface
|
|
2441
|
+
* pushes a new value into the running service.
|
|
2442
|
+
*/
|
|
2443
|
+
function pairingConfigOf(resolved) {
|
|
2444
|
+
return {
|
|
2445
|
+
tokenTtlMs: resolved.tokenTtlMs,
|
|
2446
|
+
offlineAfterMs: resolved.offlineAfterMs,
|
|
2447
|
+
maxDevices: resolved.maxDevices,
|
|
2448
|
+
cookieName: resolved.cookieName,
|
|
2449
|
+
devicesFile: resolved.devicesFile
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2193
2452
|
/** Schema defaults, re-read for hand-built test contexts (the loader applies them normally). */
|
|
2194
2453
|
const DEFAULTS = {
|
|
2195
2454
|
tokenTtlMs: 10 * 6e4,
|
|
@@ -2198,6 +2457,7 @@ const DEFAULTS = {
|
|
|
2198
2457
|
cookieName: "dsh_pair",
|
|
2199
2458
|
requirePairingForLan: true,
|
|
2200
2459
|
publicBaseUrl: void 0,
|
|
2460
|
+
devicesFile: void 0,
|
|
2201
2461
|
autoTunnel: false,
|
|
2202
2462
|
mobileEnterToSend: true,
|
|
2203
2463
|
enabled: true
|
|
@@ -2216,6 +2476,7 @@ function applyImpl(ctx, config) {
|
|
|
2216
2476
|
cookieName: config?.cookieName ?? DEFAULTS.cookieName,
|
|
2217
2477
|
requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
2218
2478
|
publicBaseUrl: config?.publicBaseUrl,
|
|
2479
|
+
devicesFile: config?.devicesFile,
|
|
2219
2480
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
2220
2481
|
mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
2221
2482
|
enabled: config?.enabled ?? DEFAULTS.enabled
|
|
@@ -2230,17 +2491,13 @@ function applyImpl(ctx, config) {
|
|
|
2230
2491
|
cookieName: value.cookieName ?? DEFAULTS.cookieName,
|
|
2231
2492
|
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
2232
2493
|
publicBaseUrl: value.publicBaseUrl,
|
|
2494
|
+
devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
|
|
2233
2495
|
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
2234
2496
|
mobileEnterToSend: value.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
2235
2497
|
enabled: value.enabled ?? DEFAULTS.enabled
|
|
2236
2498
|
};
|
|
2237
2499
|
};
|
|
2238
|
-
const service = new PairingService(
|
|
2239
|
-
tokenTtlMs: resolved.tokenTtlMs,
|
|
2240
|
-
offlineAfterMs: resolved.offlineAfterMs,
|
|
2241
|
-
maxDevices: resolved.maxDevices,
|
|
2242
|
-
cookieName: resolved.cookieName
|
|
2243
|
-
});
|
|
2500
|
+
const service = new PairingService(pairingConfigOf(resolved));
|
|
2244
2501
|
const tunnel = new TunnelManager();
|
|
2245
2502
|
let autoTunnel = resolved.autoTunnel;
|
|
2246
2503
|
tunnel.onPhase((info) => {
|
|
@@ -2342,14 +2599,13 @@ function applyImpl(ctx, config) {
|
|
|
2342
2599
|
];
|
|
2343
2600
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
2344
2601
|
ctx.effect(() => ctx.on("api/gate", gate), "remote-web-ui: api gate");
|
|
2602
|
+
new RemoteWebUiPairing(ctx, (request) => {
|
|
2603
|
+
if (!resolve().enabled) return false;
|
|
2604
|
+
return isPairedDeviceRequest(service, request);
|
|
2605
|
+
});
|
|
2345
2606
|
const sync = () => {
|
|
2346
2607
|
const value = resolve();
|
|
2347
|
-
service.config =
|
|
2348
|
-
tokenTtlMs: value.tokenTtlMs,
|
|
2349
|
-
offlineAfterMs: value.offlineAfterMs,
|
|
2350
|
-
maxDevices: value.maxDevices,
|
|
2351
|
-
cookieName: value.cookieName
|
|
2352
|
-
};
|
|
2608
|
+
service.config = pairingConfigOf(value);
|
|
2353
2609
|
autoTunnel = value.autoTunnel === true;
|
|
2354
2610
|
if (autoTunnel) {
|
|
2355
2611
|
if (value.publicBaseUrl !== void 0) console.warn("remote-web-ui: autoTunnel is on — ignoring the manually configured publicBaseUrl");
|
|
@@ -2406,4 +2662,4 @@ function isHttpUrl(value) {
|
|
|
2406
2662
|
}
|
|
2407
2663
|
}
|
|
2408
2664
|
//#endregion
|
|
2409
|
-
export { Config, REMOTE_WEB_UI_SETTINGS_NAMESPACE, apply, inject, name };
|
|
2665
|
+
export { Config, REMOTE_WEB_UI_SETTINGS_NAMESPACE, apply, inject, name, pairingConfigOf };
|