@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.0
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 +41 -19
- package/README.zh.md +17 -12
- 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 +37 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +290 -53
- package/lib/mobile.js +278 -17
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +6 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +5 -3
- package/lib/types/client/RemoteEntry.js +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/deep-link.js +1 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +15 -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/index.js +14 -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 +48 -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-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.js +1 -1
- package/lib/types/update.d.ts +3 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +34 -11
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/RemoteEntry.tsx +2 -2
- package/src/client/RemoteSettingsCard.tsx +1 -0
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +2 -0
- package/src/client/remote.module.css +26 -0
- package/src/gate.ts +15 -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/index.tsx +15 -1
- package/src/mobile/mobile-styles.ts +48 -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-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 +1 -1
- package/src/update.ts +40 -12
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. */
|
|
@@ -814,7 +909,7 @@ function makeRoutes(deps) {
|
|
|
814
909
|
if (base === void 0) throw new Error("remote-web-ui: base unavailable");
|
|
815
910
|
writeJson(res, 200, {
|
|
816
911
|
ok: true,
|
|
817
|
-
url: `${base}/?pair=${token}${workspaceId === void 0 ? "" : `&workspace=${encodeURIComponent(workspaceId)}`}`,
|
|
912
|
+
url: `${base}/m/?pair=${token}${workspaceId === void 0 ? "" : `&workspace=${encodeURIComponent(workspaceId)}`}`,
|
|
818
913
|
token,
|
|
819
914
|
expiresAt,
|
|
820
915
|
lanAddresses: service.lanAddresses,
|
|
@@ -981,23 +1076,25 @@ function makeRoutes(deps) {
|
|
|
981
1076
|
//#endregion
|
|
982
1077
|
//#region src/mobile-routes.ts
|
|
983
1078
|
/**
|
|
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.
|
|
1079
|
+
* The mobile surface's page routes: /m canonicalizes to /m/, which serves the
|
|
1080
|
+
* standalone phone UI. The page and worker live in the /m/ scope so the PWA
|
|
1081
|
+
* can cache only its static shell; the paired-device data channel remains at
|
|
1082
|
+
* /m/api and is never handled by the worker.
|
|
991
1083
|
*/
|
|
1084
|
+
const MOBILE_ROOT = "/m/";
|
|
1085
|
+
const MOBILE_BUNDLE_URL = "/m/mobile.js";
|
|
1086
|
+
const MOBILE_MANIFEST_URL = "/m/manifest.webmanifest";
|
|
1087
|
+
const MOBILE_WORKER_URL = "/m/service-worker.js";
|
|
1088
|
+
const MOBILE_OFFLINE_URL = "/m/offline.html";
|
|
992
1089
|
/** The standalone mobile bundle (built artifact, next to this file's own lib output). */
|
|
993
1090
|
function mobileBundlePath() {
|
|
994
1091
|
return fileURLToPath(new URL("../lib/mobile.js", import.meta.url));
|
|
995
1092
|
}
|
|
996
|
-
/**
|
|
997
|
-
function
|
|
998
|
-
return fileURLToPath(new URL("../assets/
|
|
1093
|
+
/** A package asset available to the host after a registry or git install. */
|
|
1094
|
+
function mobileAssetPath(name) {
|
|
1095
|
+
return fileURLToPath(new URL("../assets/" + name, import.meta.url));
|
|
999
1096
|
}
|
|
1000
|
-
/** The mobile page shell:
|
|
1097
|
+
/** The mobile page shell: self-contained, with no document-level user data. */
|
|
1001
1098
|
function pageHtml(bundleUrl) {
|
|
1002
1099
|
return [
|
|
1003
1100
|
"<!doctype html>",
|
|
@@ -1007,26 +1104,80 @@ function pageHtml(bundleUrl) {
|
|
|
1007
1104
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\">",
|
|
1008
1105
|
"<meta name=\"theme-color\" content=\"#f3f5f9\">",
|
|
1009
1106
|
"<meta name=\"referrer\" content=\"no-referrer\">",
|
|
1107
|
+
"<meta name=\"mobile-web-app-capable\" content=\"yes\">",
|
|
1108
|
+
"<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">",
|
|
1109
|
+
"<meta name=\"apple-mobile-web-app-title\" content=\"DSH Remote\">",
|
|
1110
|
+
"<link rel=\"manifest\" href=\"/m/manifest.webmanifest\">",
|
|
1010
1111
|
"<link rel=\"apple-touch-icon\" href=\"/m/apple-touch-icon.png\">",
|
|
1011
1112
|
"<title>移动端远程控制</title>",
|
|
1012
1113
|
"</head>",
|
|
1013
1114
|
"<body>",
|
|
1014
1115
|
"<div id=\"root\"></div>",
|
|
1015
|
-
|
|
1116
|
+
"<script type=\"module\" src=\"" + bundleUrl + "\"><\/script>",
|
|
1016
1117
|
"</body>",
|
|
1017
1118
|
"</html>"
|
|
1018
1119
|
].join("");
|
|
1019
1120
|
}
|
|
1020
|
-
/**
|
|
1121
|
+
/** The installable app identity and icon declarations for the /m scope. */
|
|
1122
|
+
function manifestJson() {
|
|
1123
|
+
return JSON.stringify({
|
|
1124
|
+
id: MOBILE_ROOT,
|
|
1125
|
+
name: "DSH Remote",
|
|
1126
|
+
short_name: "DSH Remote",
|
|
1127
|
+
start_url: MOBILE_ROOT,
|
|
1128
|
+
scope: MOBILE_ROOT,
|
|
1129
|
+
display: "standalone",
|
|
1130
|
+
background_color: "#151424",
|
|
1131
|
+
theme_color: "#f3f5f9",
|
|
1132
|
+
icons: [{
|
|
1133
|
+
src: "/m/icon-192.png",
|
|
1134
|
+
sizes: "192x192",
|
|
1135
|
+
type: "image/png",
|
|
1136
|
+
purpose: "any"
|
|
1137
|
+
}, {
|
|
1138
|
+
src: "/m/icon-512.png",
|
|
1139
|
+
sizes: "512x512",
|
|
1140
|
+
type: "image/png",
|
|
1141
|
+
purpose: "any maskable"
|
|
1142
|
+
}]
|
|
1143
|
+
}, null, 2);
|
|
1144
|
+
}
|
|
1145
|
+
/** A static fallback that contains no paired-device, session, or workspace data. */
|
|
1146
|
+
function offlineHtml() {
|
|
1147
|
+
return [
|
|
1148
|
+
"<!doctype html>",
|
|
1149
|
+
"<html lang=\"zh-CN\">",
|
|
1150
|
+
"<head>",
|
|
1151
|
+
"<meta charset=\"utf-8\">",
|
|
1152
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">",
|
|
1153
|
+
"<meta name=\"theme-color\" content=\"#151424\">",
|
|
1154
|
+
"<title>DSH Remote</title>",
|
|
1155
|
+
"<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>",
|
|
1156
|
+
"</head>",
|
|
1157
|
+
"<body><main><h1>DSH Remote</h1><p>Cannot reach the running DSH host. Restore the connection and reopen the app.</p></main></body>",
|
|
1158
|
+
"</html>"
|
|
1159
|
+
].join("");
|
|
1160
|
+
}
|
|
1161
|
+
/** Send a small static UTF-8 body with revalidation headers. */
|
|
1021
1162
|
function writeStatic(res, status, type, body) {
|
|
1022
1163
|
res.writeHead(status, {
|
|
1023
|
-
"content-type":
|
|
1164
|
+
"content-type": type + "; charset=utf-8",
|
|
1024
1165
|
"cache-control": "no-cache",
|
|
1025
1166
|
"referrer-policy": "no-referrer"
|
|
1026
1167
|
});
|
|
1027
1168
|
res.end(body);
|
|
1028
1169
|
}
|
|
1029
|
-
/** Send the
|
|
1170
|
+
/** Send the worker with the same scope it registers for, without HTTP caching. */
|
|
1171
|
+
function writeServiceWorker(res, body) {
|
|
1172
|
+
res.writeHead(200, {
|
|
1173
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1174
|
+
"cache-control": "no-cache",
|
|
1175
|
+
"referrer-policy": "no-referrer",
|
|
1176
|
+
"service-worker-allowed": MOBILE_ROOT
|
|
1177
|
+
});
|
|
1178
|
+
res.end(body);
|
|
1179
|
+
}
|
|
1180
|
+
/** Send a PNG body without decoding it to UTF-8. */
|
|
1030
1181
|
function writePng(res, status, body) {
|
|
1031
1182
|
res.writeHead(status, {
|
|
1032
1183
|
"content-type": "image/png",
|
|
@@ -1035,13 +1186,29 @@ function writePng(res, status, body) {
|
|
|
1035
1186
|
});
|
|
1036
1187
|
res.end(body);
|
|
1037
1188
|
}
|
|
1189
|
+
/** Canonicalize the old /m route while retaining pair and workspace query parameters. */
|
|
1190
|
+
function redirectToMobileRoot(req, res) {
|
|
1191
|
+
const url = new URL(req.url ?? "/m", "http://x");
|
|
1192
|
+
res.writeHead(308, {
|
|
1193
|
+
location: MOBILE_ROOT + url.search,
|
|
1194
|
+
"cache-control": "no-store",
|
|
1195
|
+
"referrer-policy": "no-referrer"
|
|
1196
|
+
});
|
|
1197
|
+
res.end();
|
|
1198
|
+
}
|
|
1038
1199
|
/**
|
|
1039
1200
|
* Build the mobile page routes.
|
|
1040
|
-
* @returns the
|
|
1201
|
+
* @returns Exact routes for the canonical page, static shell, and PWA assets.
|
|
1041
1202
|
*/
|
|
1042
1203
|
function makeMobileRoutes() {
|
|
1043
1204
|
const handlePage = (_req, res) => {
|
|
1044
|
-
writeStatic(res, 200, "text/html", pageHtml(
|
|
1205
|
+
writeStatic(res, 200, "text/html", pageHtml(MOBILE_BUNDLE_URL));
|
|
1206
|
+
};
|
|
1207
|
+
const handleManifest = (_req, res) => {
|
|
1208
|
+
writeStatic(res, 200, "application/manifest+json", manifestJson());
|
|
1209
|
+
};
|
|
1210
|
+
const handleOffline = (_req, res) => {
|
|
1211
|
+
writeStatic(res, 200, "text/html", offlineHtml());
|
|
1045
1212
|
};
|
|
1046
1213
|
let bundleBody;
|
|
1047
1214
|
const handleBundle = async (_req, res) => {
|
|
@@ -1060,33 +1227,80 @@ function makeMobileRoutes() {
|
|
|
1060
1227
|
}
|
|
1061
1228
|
writeStatic(res, 200, "text/javascript", bundleBody);
|
|
1062
1229
|
};
|
|
1063
|
-
|
|
1064
|
-
|
|
1230
|
+
let workerBody;
|
|
1231
|
+
const handleWorker = async (_req, res) => {
|
|
1232
|
+
if (workerBody === void 0) {
|
|
1233
|
+
const path = mobileAssetPath("mobile-service-worker.js");
|
|
1234
|
+
if (!existsSync(path)) {
|
|
1235
|
+
writeStatic(res, 503, "text/plain", "mobile service worker not found");
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
workerBody = await readFile(path, "utf8");
|
|
1240
|
+
} catch {
|
|
1241
|
+
writeStatic(res, 500, "text/plain", "failed to read the mobile service worker");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
writeServiceWorker(res, workerBody);
|
|
1246
|
+
};
|
|
1247
|
+
const makeIconHandler = (asset) => async (_req, res) => {
|
|
1248
|
+
const path = mobileAssetPath(asset);
|
|
1065
1249
|
if (!existsSync(path)) {
|
|
1066
|
-
writeStatic(res, 404, "text/plain", "
|
|
1250
|
+
writeStatic(res, 404, "text/plain", asset + " not found");
|
|
1067
1251
|
return;
|
|
1068
1252
|
}
|
|
1069
1253
|
try {
|
|
1070
1254
|
writePng(res, 200, await readFile(path));
|
|
1071
1255
|
} catch {
|
|
1072
|
-
writeStatic(res, 500, "text/plain", "failed to read
|
|
1256
|
+
writeStatic(res, 500, "text/plain", "failed to read " + asset);
|
|
1073
1257
|
}
|
|
1074
1258
|
};
|
|
1075
1259
|
return [
|
|
1076
1260
|
{
|
|
1077
1261
|
kind: "exact",
|
|
1078
1262
|
path: "/m",
|
|
1263
|
+
handler: redirectToMobileRoot
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
kind: "exact",
|
|
1267
|
+
path: MOBILE_ROOT,
|
|
1079
1268
|
handler: handlePage
|
|
1080
1269
|
},
|
|
1081
1270
|
{
|
|
1082
1271
|
kind: "exact",
|
|
1083
|
-
path:
|
|
1272
|
+
path: MOBILE_BUNDLE_URL,
|
|
1084
1273
|
handler: handleBundle
|
|
1085
1274
|
},
|
|
1275
|
+
{
|
|
1276
|
+
kind: "exact",
|
|
1277
|
+
path: MOBILE_MANIFEST_URL,
|
|
1278
|
+
handler: handleManifest
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
kind: "exact",
|
|
1282
|
+
path: MOBILE_WORKER_URL,
|
|
1283
|
+
handler: handleWorker
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
kind: "exact",
|
|
1287
|
+
path: MOBILE_OFFLINE_URL,
|
|
1288
|
+
handler: handleOffline
|
|
1289
|
+
},
|
|
1086
1290
|
{
|
|
1087
1291
|
kind: "exact",
|
|
1088
1292
|
path: "/m/apple-touch-icon.png",
|
|
1089
|
-
handler:
|
|
1293
|
+
handler: makeIconHandler("apple-touch-icon.png")
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
kind: "exact",
|
|
1297
|
+
path: "/m/icon-192.png",
|
|
1298
|
+
handler: makeIconHandler("mobile-icon-192.png")
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
kind: "exact",
|
|
1302
|
+
path: "/m/icon-512.png",
|
|
1303
|
+
handler: makeIconHandler("mobile-icon-512.png")
|
|
1090
1304
|
}
|
|
1091
1305
|
];
|
|
1092
1306
|
}
|
|
@@ -1715,12 +1929,13 @@ function resolveUpdateTarget(deps) {
|
|
|
1715
1929
|
if (anchor === void 0) return { error: "not-found" };
|
|
1716
1930
|
const profile = findProfile(manifestPath);
|
|
1717
1931
|
if (profile === void 0) return { error: "link" };
|
|
1718
|
-
const
|
|
1932
|
+
const profileManifest = readManifest(join(profile.dir, "package.json"));
|
|
1933
|
+
const spec = (profileManifest?.dependencies)?.[anchor];
|
|
1719
1934
|
if (isLinkedSpec(spec)) return { error: "link" };
|
|
1720
1935
|
return {
|
|
1721
1936
|
profileName: profile.name,
|
|
1722
1937
|
profileDir: profile.dir,
|
|
1723
|
-
packages:
|
|
1938
|
+
packages: familyUpdatePackages(anchor, manifest, profileManifest)
|
|
1724
1939
|
};
|
|
1725
1940
|
}
|
|
1726
1941
|
/** Family children of the anchor: its dependencies under the family scope. */
|
|
@@ -1731,6 +1946,14 @@ function familyChildren(anchorManifest) {
|
|
|
1731
1946
|
for (const [name, spec] of Object.entries(dependencies)) if (name.startsWith("@linxin666/") && typeof spec === "string") names.push(name);
|
|
1732
1947
|
return names;
|
|
1733
1948
|
}
|
|
1949
|
+
/** Registry-managed family packages covered by one update operation. */
|
|
1950
|
+
function familyUpdatePackages(anchor, anchorManifest, profileManifest) {
|
|
1951
|
+
const names = /* @__PURE__ */ new Set([anchor, ...familyChildren(anchorManifest)]);
|
|
1952
|
+
const dependencies = profileManifest?.dependencies;
|
|
1953
|
+
if (typeof dependencies !== "object" || dependencies === null) return [...names];
|
|
1954
|
+
for (const [name, spec] of Object.entries(dependencies)) if (name.startsWith("@linxin666/") && typeof spec === "string" && !isLinkedSpec(spec)) names.add(name);
|
|
1955
|
+
return [...names];
|
|
1956
|
+
}
|
|
1734
1957
|
/**
|
|
1735
1958
|
* Probe the npm registry for one package's latest release.
|
|
1736
1959
|
* @param name - the package name (scope slash URL-encoded).
|
|
@@ -1766,14 +1989,15 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
|
|
|
1766
1989
|
*/
|
|
1767
1990
|
const VERSION_UNKNOWN = "0.0.0";
|
|
1768
1991
|
/** The resolved current version of one family package (probe failure tolerated). */
|
|
1769
|
-
function readInstalledVersion(resolve, name) {
|
|
1992
|
+
function readInstalledVersion(resolve, name, profileDir) {
|
|
1770
1993
|
try {
|
|
1771
1994
|
const path = resolve(name + "/package.json");
|
|
1772
1995
|
const version = path === void 0 ? void 0 : readManifest(path)?.version;
|
|
1773
|
-
|
|
1774
|
-
} catch {
|
|
1775
|
-
|
|
1776
|
-
|
|
1996
|
+
if (typeof version === "string") return version;
|
|
1997
|
+
} catch {}
|
|
1998
|
+
if (profileDir === void 0 || !/^@linxin666\/[a-z0-9][a-z0-9._-]*$/.test(name)) return VERSION_UNKNOWN;
|
|
1999
|
+
const version = readManifest(join(profileDir, "node_modules", ...name.split("/"), "package.json"))?.version;
|
|
2000
|
+
return typeof version === "string" ? version : VERSION_UNKNOWN;
|
|
1777
2001
|
}
|
|
1778
2002
|
/**
|
|
1779
2003
|
* Build the update status: locate the anchor, detect the install mode, and
|
|
@@ -1808,14 +2032,14 @@ async function checkUpdates(deps) {
|
|
|
1808
2032
|
packages: [],
|
|
1809
2033
|
outdated: false
|
|
1810
2034
|
};
|
|
1811
|
-
const names =
|
|
2035
|
+
const names = familyUpdatePackages(anchor, manifest, profileManifest);
|
|
1812
2036
|
const latestList = await Promise.all(names.map((name) => deps.fetchLatest(name)));
|
|
1813
2037
|
const packages = [];
|
|
1814
2038
|
let probeFailures = 0;
|
|
1815
2039
|
names.forEach((name, index) => {
|
|
1816
2040
|
const latest = latestList[index];
|
|
1817
2041
|
if (latest === void 0) probeFailures++;
|
|
1818
|
-
const current = readInstalledVersion(deps.resolve, name);
|
|
2042
|
+
const current = readInstalledVersion(deps.resolve, name, profile.dir);
|
|
1819
2043
|
packages.push({
|
|
1820
2044
|
name,
|
|
1821
2045
|
current,
|
|
@@ -2027,7 +2251,7 @@ function runUpdate(deps) {
|
|
|
2027
2251
|
async function runUpdateVerified(deps) {
|
|
2028
2252
|
const before = /* @__PURE__ */ new Map();
|
|
2029
2253
|
for (const name of deps.run.packages) {
|
|
2030
|
-
const version = readInstalledVersion(deps.check.resolve, name);
|
|
2254
|
+
const version = readInstalledVersion(deps.check.resolve, name, deps.run.profileDir);
|
|
2031
2255
|
if (version !== VERSION_UNKNOWN) before.set(name, version);
|
|
2032
2256
|
}
|
|
2033
2257
|
const result = await runUpdate(deps.run);
|
|
@@ -2184,12 +2408,28 @@ const Config = z.object({
|
|
|
2184
2408
|
cookieName: z.string().min(1).default("dsh_pair"),
|
|
2185
2409
|
requirePairingForLan: z.boolean().default(true),
|
|
2186
2410
|
publicBaseUrl: z.string(),
|
|
2411
|
+
devicesFile: z.string(),
|
|
2187
2412
|
autoTunnel: z.boolean().default(false),
|
|
2188
2413
|
mobileEnterToSend: z.boolean().default(true),
|
|
2189
2414
|
enabled: z.boolean().default(true)
|
|
2190
2415
|
});
|
|
2191
2416
|
/** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
|
|
2192
2417
|
const SWEEP_INTERVAL_MS = 1e4;
|
|
2418
|
+
/**
|
|
2419
|
+
* The single mapping from resolved plugin config to the pairing service
|
|
2420
|
+
* config. Both the constructed service and every live settings sync reuse
|
|
2421
|
+
* it, so no field can be silently dropped when the web settings surface
|
|
2422
|
+
* pushes a new value into the running service.
|
|
2423
|
+
*/
|
|
2424
|
+
function pairingConfigOf(resolved) {
|
|
2425
|
+
return {
|
|
2426
|
+
tokenTtlMs: resolved.tokenTtlMs,
|
|
2427
|
+
offlineAfterMs: resolved.offlineAfterMs,
|
|
2428
|
+
maxDevices: resolved.maxDevices,
|
|
2429
|
+
cookieName: resolved.cookieName,
|
|
2430
|
+
devicesFile: resolved.devicesFile
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2193
2433
|
/** Schema defaults, re-read for hand-built test contexts (the loader applies them normally). */
|
|
2194
2434
|
const DEFAULTS = {
|
|
2195
2435
|
tokenTtlMs: 10 * 6e4,
|
|
@@ -2198,6 +2438,7 @@ const DEFAULTS = {
|
|
|
2198
2438
|
cookieName: "dsh_pair",
|
|
2199
2439
|
requirePairingForLan: true,
|
|
2200
2440
|
publicBaseUrl: void 0,
|
|
2441
|
+
devicesFile: void 0,
|
|
2201
2442
|
autoTunnel: false,
|
|
2202
2443
|
mobileEnterToSend: true,
|
|
2203
2444
|
enabled: true
|
|
@@ -2216,6 +2457,7 @@ function applyImpl(ctx, config) {
|
|
|
2216
2457
|
cookieName: config?.cookieName ?? DEFAULTS.cookieName,
|
|
2217
2458
|
requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
2218
2459
|
publicBaseUrl: config?.publicBaseUrl,
|
|
2460
|
+
devicesFile: config?.devicesFile,
|
|
2219
2461
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
2220
2462
|
mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
2221
2463
|
enabled: config?.enabled ?? DEFAULTS.enabled
|
|
@@ -2230,17 +2472,13 @@ function applyImpl(ctx, config) {
|
|
|
2230
2472
|
cookieName: value.cookieName ?? DEFAULTS.cookieName,
|
|
2231
2473
|
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
2232
2474
|
publicBaseUrl: value.publicBaseUrl,
|
|
2475
|
+
devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
|
|
2233
2476
|
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
2234
2477
|
mobileEnterToSend: value.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
2235
2478
|
enabled: value.enabled ?? DEFAULTS.enabled
|
|
2236
2479
|
};
|
|
2237
2480
|
};
|
|
2238
|
-
const service = new PairingService(
|
|
2239
|
-
tokenTtlMs: resolved.tokenTtlMs,
|
|
2240
|
-
offlineAfterMs: resolved.offlineAfterMs,
|
|
2241
|
-
maxDevices: resolved.maxDevices,
|
|
2242
|
-
cookieName: resolved.cookieName
|
|
2243
|
-
});
|
|
2481
|
+
const service = new PairingService(pairingConfigOf(resolved));
|
|
2244
2482
|
const tunnel = new TunnelManager();
|
|
2245
2483
|
let autoTunnel = resolved.autoTunnel;
|
|
2246
2484
|
tunnel.onPhase((info) => {
|
|
@@ -2342,14 +2580,13 @@ function applyImpl(ctx, config) {
|
|
|
2342
2580
|
];
|
|
2343
2581
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
2344
2582
|
ctx.effect(() => ctx.on("api/gate", gate), "remote-web-ui: api gate");
|
|
2583
|
+
new RemoteWebUiPairing(ctx, (request) => {
|
|
2584
|
+
if (!resolve().enabled) return false;
|
|
2585
|
+
return isPairedDeviceRequest(service, request);
|
|
2586
|
+
});
|
|
2345
2587
|
const sync = () => {
|
|
2346
2588
|
const value = resolve();
|
|
2347
|
-
service.config =
|
|
2348
|
-
tokenTtlMs: value.tokenTtlMs,
|
|
2349
|
-
offlineAfterMs: value.offlineAfterMs,
|
|
2350
|
-
maxDevices: value.maxDevices,
|
|
2351
|
-
cookieName: value.cookieName
|
|
2352
|
-
};
|
|
2589
|
+
service.config = pairingConfigOf(value);
|
|
2353
2590
|
autoTunnel = value.autoTunnel === true;
|
|
2354
2591
|
if (autoTunnel) {
|
|
2355
2592
|
if (value.publicBaseUrl !== void 0) console.warn("remote-web-ui: autoTunnel is on — ignoring the manually configured publicBaseUrl");
|
|
@@ -2406,4 +2643,4 @@ function isHttpUrl(value) {
|
|
|
2406
2643
|
}
|
|
2407
2644
|
}
|
|
2408
2645
|
//#endregion
|
|
2409
|
-
export { Config, REMOTE_WEB_UI_SETTINGS_NAMESPACE, apply, inject, name };
|
|
2646
|
+
export { Config, REMOTE_WEB_UI_SETTINGS_NAMESPACE, apply, inject, name, pairingConfigOf };
|