@linxin666/dsh-remote-web-ui 0.1.19 → 0.1.20
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/lib/client.js +20 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +119 -78
- package/lib/mobile.js +18 -5
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +41 -11
- package/lib/types/client/RemotePanel.d.ts +1 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +2 -2
- package/lib/types/client/UpdateEntry.js +1 -1
- package/lib/types/client/UpdatePanel.js +1 -1
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/index.js +7 -7
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/gate.js +1 -1
- package/lib/types/http.d.ts +14 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/http.js +27 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +12 -10
- package/lib/types/invariant.d.ts.map +1 -1
- package/lib/types/mobile/api.js +1 -1
- package/lib/types/mobile/index.js +3 -3
- package/lib/types/mobile/markdown.d.ts +6 -4
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +8 -4
- package/lib/types/mobile/markdown.test.js +5 -1
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +14 -1
- package/lib/types/mobile/messages.test.js +1 -1
- package/lib/types/mobile/mobile-theme.test.js +2 -2
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +1 -1
- package/lib/types/mobile/mux.test.js +1 -1
- package/lib/types/mobile/rpc.d.ts.map +1 -1
- package/lib/types/mobile/theme-toggle.js +1 -1
- package/lib/types/mobile/views/App.d.ts +0 -11
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +6 -6
- package/lib/types/mobile/views/ChatView.js +6 -6
- package/lib/types/mobile/views/ChatView.test.js +3 -3
- package/lib/types/mobile/views/SessionListView.js +3 -3
- package/lib/types/mobile/views/SessionListView.test.js +2 -2
- package/lib/types/mobile/views/WorkspaceView.js +3 -3
- package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
- package/lib/types/mobile-api.d.ts +1 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +11 -18
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +17 -11
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/lib/types/mount-once.js +42 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/routes.d.ts +6 -6
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +14 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/update-routes.d.ts +2 -2
- package/lib/types/update-routes.d.ts.map +1 -1
- package/lib/types/update-routes.js +1 -6
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +6 -3
- package/package.json +1 -1
- package/src/client/RemoteEntry.tsx +38 -6
- package/src/client/RemotePanel.tsx +1 -1
- package/src/http.ts +30 -0
- package/src/index.ts +4 -1
- package/src/mobile/markdown.test.ts +5 -0
- package/src/mobile/markdown.ts +7 -4
- package/src/mobile/messages.ts +14 -1
- package/src/mobile/views/App.tsx +0 -12
- package/src/mobile-api.ts +9 -17
- package/src/mobile-routes.ts +16 -10
- package/src/mount-once.ts +48 -0
- package/src/routes.ts +11 -17
- package/src/update-routes.ts +1 -7
- package/src/update.ts +6 -3
package/lib/index.js
CHANGED
|
@@ -572,6 +572,32 @@ function makeGateListener(service, requirePairingForLan = true, enabled = true)
|
|
|
572
572
|
};
|
|
573
573
|
}
|
|
574
574
|
//#endregion
|
|
575
|
+
//#region src/http.ts
|
|
576
|
+
/** One JSON response. */
|
|
577
|
+
function writeJson(res, status, body) {
|
|
578
|
+
const payload = JSON.stringify(body);
|
|
579
|
+
res.writeHead(status, {
|
|
580
|
+
"content-type": "application/json; charset=utf-8",
|
|
581
|
+
"referrer-policy": "no-referrer"
|
|
582
|
+
});
|
|
583
|
+
res.end(payload);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Read a request body up to maxBytes and parse it as JSON.
|
|
587
|
+
* @throws 'body too large' beyond the cap, or the JSON.parse error.
|
|
588
|
+
*/
|
|
589
|
+
async function readBoundedJson(req, maxBytes) {
|
|
590
|
+
const chunks = [];
|
|
591
|
+
let size = 0;
|
|
592
|
+
for await (const chunk of req) {
|
|
593
|
+
const buffer = chunk;
|
|
594
|
+
size += buffer.length;
|
|
595
|
+
if (size > maxBytes) throw new Error("body too large");
|
|
596
|
+
chunks.push(buffer);
|
|
597
|
+
}
|
|
598
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
575
601
|
//#region src/routes.ts
|
|
576
602
|
/**
|
|
577
603
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
@@ -663,27 +689,10 @@ function parsePairPayload(schema, body) {
|
|
|
663
689
|
const result = schema.safeParse(body ?? {});
|
|
664
690
|
return result.success ? result.data : void 0;
|
|
665
691
|
}
|
|
666
|
-
/**
|
|
667
|
-
function writeJson$1(res, status, body) {
|
|
668
|
-
const payload = JSON.stringify(body);
|
|
669
|
-
res.writeHead(status, {
|
|
670
|
-
"content-type": "application/json; charset=utf-8",
|
|
671
|
-
"referrer-policy": "no-referrer"
|
|
672
|
-
});
|
|
673
|
-
res.end(payload);
|
|
674
|
-
}
|
|
675
|
-
/** Read a request body up to MAX_BODY_BYTES and parse it as JSON. */
|
|
692
|
+
/** Read a request body up to MAX_BODY_BYTES and parse it as JSON (undefined on failure). */
|
|
676
693
|
async function readJsonBody$1(req) {
|
|
677
|
-
const chunks = [];
|
|
678
|
-
let size = 0;
|
|
679
|
-
for await (const chunk of req) {
|
|
680
|
-
const buffer = chunk;
|
|
681
|
-
size += buffer.length;
|
|
682
|
-
if (size > MAX_BODY_BYTES) return void 0;
|
|
683
|
-
chunks.push(buffer);
|
|
684
|
-
}
|
|
685
694
|
try {
|
|
686
|
-
const parsed =
|
|
695
|
+
const parsed = await readBoundedJson(req, MAX_BODY_BYTES);
|
|
687
696
|
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
688
697
|
} catch {
|
|
689
698
|
return;
|
|
@@ -766,6 +775,9 @@ function makeRoutes(deps) {
|
|
|
766
775
|
const rateLimitAccept = (req) => {
|
|
767
776
|
const ip = req.socket?.remoteAddress ?? "unknown";
|
|
768
777
|
const nowMs = Date.now();
|
|
778
|
+
if (acceptAttempts.size > 256) {
|
|
779
|
+
for (const [key, attempt] of acceptAttempts) if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS) acceptAttempts.delete(key);
|
|
780
|
+
}
|
|
769
781
|
const entry = acceptAttempts.get(ip);
|
|
770
782
|
if (entry === void 0 || nowMs - entry.windowStart > ACCEPT_WINDOW_MS) {
|
|
771
783
|
acceptAttempts.set(ip, {
|
|
@@ -780,7 +792,7 @@ function makeRoutes(deps) {
|
|
|
780
792
|
const handleIssue = async (req, res) => {
|
|
781
793
|
if (!requireMethod(req, res, "POST")) return;
|
|
782
794
|
if (!loopbackFence(req)) {
|
|
783
|
-
writeJson
|
|
795
|
+
writeJson(res, 403, {
|
|
784
796
|
ok: false,
|
|
785
797
|
code: "forbidden"
|
|
786
798
|
});
|
|
@@ -789,7 +801,7 @@ function makeRoutes(deps) {
|
|
|
789
801
|
const body = await readJsonBody$1(req);
|
|
790
802
|
const payload = parsePairPayload(issuePayloadSchema, body);
|
|
791
803
|
if (payload === void 0) {
|
|
792
|
-
writeJson
|
|
804
|
+
writeJson(res, 400, {
|
|
793
805
|
ok: false,
|
|
794
806
|
code: "bad-payload"
|
|
795
807
|
});
|
|
@@ -800,7 +812,7 @@ function makeRoutes(deps) {
|
|
|
800
812
|
const { token, expiresAt } = service.issue(workspaceId, address);
|
|
801
813
|
const base = address === void 0 ? service.publicBaseUrl ?? service.lanBaseUrl : service.lanBaseUrlFor(address);
|
|
802
814
|
if (base === void 0) throw new Error("remote-web-ui: base unavailable");
|
|
803
|
-
writeJson
|
|
815
|
+
writeJson(res, 200, {
|
|
804
816
|
ok: true,
|
|
805
817
|
url: `${base}/?pair=${token}${workspaceId === void 0 ? "" : `&workspace=${encodeURIComponent(workspaceId)}`}`,
|
|
806
818
|
token,
|
|
@@ -810,7 +822,7 @@ function makeRoutes(deps) {
|
|
|
810
822
|
});
|
|
811
823
|
} catch (error) {
|
|
812
824
|
const unknownAddress = error instanceof UnknownLanAddressError;
|
|
813
|
-
writeJson
|
|
825
|
+
writeJson(res, unknownAddress ? 400 : 409, {
|
|
814
826
|
ok: false,
|
|
815
827
|
code: unknownAddress ? "unknown-address" : "lan-required"
|
|
816
828
|
});
|
|
@@ -819,14 +831,14 @@ function makeRoutes(deps) {
|
|
|
819
831
|
const handleAccept = async (req, res) => {
|
|
820
832
|
if (!requireMethod(req, res, "POST")) return;
|
|
821
833
|
if (!lanFence(req)) {
|
|
822
|
-
writeJson
|
|
834
|
+
writeJson(res, 403, {
|
|
823
835
|
ok: false,
|
|
824
836
|
code: "forbidden"
|
|
825
837
|
});
|
|
826
838
|
return;
|
|
827
839
|
}
|
|
828
840
|
if (rateLimitAccept(req)) {
|
|
829
|
-
writeJson
|
|
841
|
+
writeJson(res, 429, {
|
|
830
842
|
ok: false,
|
|
831
843
|
code: "rate-limited"
|
|
832
844
|
});
|
|
@@ -835,7 +847,7 @@ function makeRoutes(deps) {
|
|
|
835
847
|
const body = await readJsonBody$1(req);
|
|
836
848
|
const payload = parsePairPayload(acceptPayloadSchema, body);
|
|
837
849
|
if (payload === void 0) {
|
|
838
|
-
writeJson
|
|
850
|
+
writeJson(res, 400, {
|
|
839
851
|
ok: false,
|
|
840
852
|
code: "bad-payload"
|
|
841
853
|
});
|
|
@@ -843,7 +855,7 @@ function makeRoutes(deps) {
|
|
|
843
855
|
}
|
|
844
856
|
const result = service.accept(payload.token);
|
|
845
857
|
if (!result.ok) {
|
|
846
|
-
writeJson
|
|
858
|
+
writeJson(res, result.code === "used" ? 409 : 404, {
|
|
847
859
|
ok: false,
|
|
848
860
|
code: result.code
|
|
849
861
|
});
|
|
@@ -861,7 +873,7 @@ function makeRoutes(deps) {
|
|
|
861
873
|
const handleStop = async (req, res) => {
|
|
862
874
|
if (!requireMethod(req, res, "POST")) return;
|
|
863
875
|
if (!loopbackFence(req)) {
|
|
864
|
-
writeJson
|
|
876
|
+
writeJson(res, 403, {
|
|
865
877
|
ok: false,
|
|
866
878
|
code: "forbidden"
|
|
867
879
|
});
|
|
@@ -869,19 +881,19 @@ function makeRoutes(deps) {
|
|
|
869
881
|
}
|
|
870
882
|
const body = await readJsonBody$1(req);
|
|
871
883
|
if (parsePairPayload(pairActionPayloadSchema, body) === void 0) {
|
|
872
|
-
writeJson
|
|
884
|
+
writeJson(res, 400, {
|
|
873
885
|
ok: false,
|
|
874
886
|
code: "bad-payload"
|
|
875
887
|
});
|
|
876
888
|
return;
|
|
877
889
|
}
|
|
878
890
|
service.stop();
|
|
879
|
-
writeJson
|
|
891
|
+
writeJson(res, 200, { ok: true });
|
|
880
892
|
};
|
|
881
893
|
const handleHeartbeat = async (req, res) => {
|
|
882
894
|
if (!requireMethod(req, res, "POST")) return;
|
|
883
895
|
if (!lanFence(req)) {
|
|
884
|
-
writeJson
|
|
896
|
+
writeJson(res, 403, {
|
|
885
897
|
ok: false,
|
|
886
898
|
code: "forbidden"
|
|
887
899
|
});
|
|
@@ -889,7 +901,7 @@ function makeRoutes(deps) {
|
|
|
889
901
|
}
|
|
890
902
|
const body = await readJsonBody$1(req);
|
|
891
903
|
if (parsePairPayload(pairActionPayloadSchema, body) === void 0) {
|
|
892
|
-
writeJson
|
|
904
|
+
writeJson(res, 400, {
|
|
893
905
|
ok: false,
|
|
894
906
|
code: "bad-payload"
|
|
895
907
|
});
|
|
@@ -897,25 +909,25 @@ function makeRoutes(deps) {
|
|
|
897
909
|
}
|
|
898
910
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
899
911
|
if (deviceId === void 0 || !service.heartbeat(deviceId)) {
|
|
900
|
-
writeJson
|
|
912
|
+
writeJson(res, 401, {
|
|
901
913
|
ok: false,
|
|
902
914
|
code: "unpaired"
|
|
903
915
|
});
|
|
904
916
|
return;
|
|
905
917
|
}
|
|
906
|
-
writeJson
|
|
918
|
+
writeJson(res, 200, { ok: true });
|
|
907
919
|
};
|
|
908
920
|
const handleStatus = async (req, res) => {
|
|
909
921
|
if (!requireMethod(req, res, "GET")) return;
|
|
910
922
|
if (!lanFence(req)) {
|
|
911
|
-
writeJson
|
|
923
|
+
writeJson(res, 403, {
|
|
912
924
|
ok: false,
|
|
913
925
|
code: "forbidden"
|
|
914
926
|
});
|
|
915
927
|
return;
|
|
916
928
|
}
|
|
917
929
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
918
|
-
writeJson
|
|
930
|
+
writeJson(res, 200, {
|
|
919
931
|
ok: true,
|
|
920
932
|
paired: deviceId !== void 0 && service.hasDevice(deviceId),
|
|
921
933
|
...service.snapshot()
|
|
@@ -924,7 +936,7 @@ function makeRoutes(deps) {
|
|
|
924
936
|
const handleEvents = (req, res) => {
|
|
925
937
|
if (!requireMethod(req, res, "GET")) return;
|
|
926
938
|
if (!loopbackFence(req)) {
|
|
927
|
-
writeJson
|
|
939
|
+
writeJson(res, 403, {
|
|
928
940
|
ok: false,
|
|
929
941
|
code: "forbidden"
|
|
930
942
|
});
|
|
@@ -1031,17 +1043,22 @@ function makeMobileRoutes() {
|
|
|
1031
1043
|
const handlePage = (_req, res) => {
|
|
1032
1044
|
writeStatic(res, 200, "text/html", pageHtml("/m/mobile.js"));
|
|
1033
1045
|
};
|
|
1046
|
+
let bundleBody;
|
|
1034
1047
|
const handleBundle = async (_req, res) => {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1048
|
+
if (bundleBody === void 0) {
|
|
1049
|
+
const path = mobileBundlePath();
|
|
1050
|
+
if (!existsSync(path)) {
|
|
1051
|
+
writeStatic(res, 503, "text/plain", "mobile bundle not built: run pnpm --filter @linxin666/dsh-remote-web-ui build");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
bundleBody = await readFile(path, "utf8");
|
|
1056
|
+
} catch {
|
|
1057
|
+
writeStatic(res, 500, "text/plain", "failed to read the mobile bundle");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1044
1060
|
}
|
|
1061
|
+
writeStatic(res, 200, "text/javascript", bundleBody);
|
|
1045
1062
|
};
|
|
1046
1063
|
const handleIcon = async (_req, res) => {
|
|
1047
1064
|
const path = appleTouchIconPath();
|
|
@@ -1147,10 +1164,6 @@ function makeMobileApiRoutes(deps) {
|
|
|
1147
1164
|
const gateOk = (req) => {
|
|
1148
1165
|
return touchDeviceFor(req);
|
|
1149
1166
|
};
|
|
1150
|
-
const writeJson = (res, status, body) => {
|
|
1151
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
1152
|
-
res.end(JSON.stringify(body));
|
|
1153
|
-
};
|
|
1154
1167
|
const handleMethod = async (req, res) => {
|
|
1155
1168
|
if (req.method !== "POST") {
|
|
1156
1169
|
res.writeHead(405);
|
|
@@ -1227,10 +1240,12 @@ function makeMobileApiRoutes(deps) {
|
|
|
1227
1240
|
return;
|
|
1228
1241
|
}
|
|
1229
1242
|
try {
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1243
|
+
const abort = new AbortController();
|
|
1244
|
+
res.on("close", () => {
|
|
1245
|
+
if (!res.writableEnded) abort.abort();
|
|
1246
|
+
});
|
|
1247
|
+
writeJson(res, 200, await dispatch(apiProxy, method, parsed?.payload, rpcId, abort.signal));
|
|
1232
1248
|
} catch (error) {
|
|
1233
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1234
1249
|
writeJson(res, 200, {
|
|
1235
1250
|
type: "server-response",
|
|
1236
1251
|
rpcId,
|
|
@@ -1238,7 +1253,7 @@ function makeMobileApiRoutes(deps) {
|
|
|
1238
1253
|
ok: false,
|
|
1239
1254
|
error: {
|
|
1240
1255
|
code: "internal",
|
|
1241
|
-
message
|
|
1256
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1242
1257
|
}
|
|
1243
1258
|
}
|
|
1244
1259
|
});
|
|
@@ -1305,18 +1320,10 @@ function makeMobileApiRoutes(deps) {
|
|
|
1305
1320
|
}
|
|
1306
1321
|
/** Read a request body as JSON (bounded). */
|
|
1307
1322
|
async function readJsonBody(req) {
|
|
1308
|
-
|
|
1309
|
-
let size = 0;
|
|
1310
|
-
for await (const chunk of req) {
|
|
1311
|
-
const buffer = chunk;
|
|
1312
|
-
size += buffer.length;
|
|
1313
|
-
if (size > 64 * 1024) throw new Error("body too large");
|
|
1314
|
-
chunks.push(buffer);
|
|
1315
|
-
}
|
|
1316
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1323
|
+
return readBoundedJson(req, 64 * 1024);
|
|
1317
1324
|
}
|
|
1318
1325
|
/** Dispatch one allowlisted method through the host ApiProxy. */
|
|
1319
|
-
async function dispatch(apiProxy, method, payload, rpcId) {
|
|
1326
|
+
async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
1320
1327
|
const request = {
|
|
1321
1328
|
rpcId: RpcId(rpcId),
|
|
1322
1329
|
payload
|
|
@@ -1354,7 +1361,7 @@ async function dispatch(apiProxy, method, payload, rpcId) {
|
|
|
1354
1361
|
if (method === "workspace.list") return wrap(await apiProxy.workspace.list(request));
|
|
1355
1362
|
if (method === "session.create") return wrap(await apiProxy.sessions.create(request));
|
|
1356
1363
|
if (method === "session.history") return wrap(await apiProxy.sessions.history(request));
|
|
1357
|
-
if (method === "session.search") return wrap(await apiProxy.sessions.search(request, new AbortController().signal));
|
|
1364
|
+
if (method === "session.search") return wrap(await apiProxy.sessions.search(request, signal ?? new AbortController().signal));
|
|
1358
1365
|
if (method === "session.prompt") return wrap(await apiProxy.sessions.prompt(request));
|
|
1359
1366
|
if (method === "session.models") return wrap(await apiProxy.sessions.models(request));
|
|
1360
1367
|
if (method === "session.selectModel") return wrap(await apiProxy.sessions.selectModel(request));
|
|
@@ -1802,10 +1809,11 @@ async function checkUpdates(deps) {
|
|
|
1802
1809
|
outdated: false
|
|
1803
1810
|
};
|
|
1804
1811
|
const names = [anchor, ...familyChildren(manifest)];
|
|
1812
|
+
const latestList = await Promise.all(names.map((name) => deps.fetchLatest(name)));
|
|
1805
1813
|
const packages = [];
|
|
1806
1814
|
let probeFailures = 0;
|
|
1807
|
-
|
|
1808
|
-
const latest =
|
|
1815
|
+
names.forEach((name, index) => {
|
|
1816
|
+
const latest = latestList[index];
|
|
1809
1817
|
if (latest === void 0) probeFailures++;
|
|
1810
1818
|
const current = readInstalledVersion(deps.resolve, name);
|
|
1811
1819
|
packages.push({
|
|
@@ -1814,7 +1822,7 @@ async function checkUpdates(deps) {
|
|
|
1814
1822
|
latest,
|
|
1815
1823
|
outdated: latest !== void 0 && latest !== current && compareVersions(latest, current) > 0
|
|
1816
1824
|
});
|
|
1817
|
-
}
|
|
1825
|
+
});
|
|
1818
1826
|
const error = probeFailures === names.length && names.length > 0 ? "registry-unreachable" : void 0;
|
|
1819
1827
|
return {
|
|
1820
1828
|
mode: linked ? "link" : "npm",
|
|
@@ -2061,15 +2069,6 @@ const UPDATE_PATHS = {
|
|
|
2061
2069
|
status: "/api/update/status",
|
|
2062
2070
|
run: "/api/update/run"
|
|
2063
2071
|
};
|
|
2064
|
-
/** One JSON response. */
|
|
2065
|
-
function writeJson(res, status, body) {
|
|
2066
|
-
const payload = JSON.stringify(body);
|
|
2067
|
-
res.writeHead(status, {
|
|
2068
|
-
"content-type": "application/json; charset=utf-8",
|
|
2069
|
-
"referrer-policy": "no-referrer"
|
|
2070
|
-
});
|
|
2071
|
-
res.end(payload);
|
|
2072
|
-
}
|
|
2073
2072
|
/**
|
|
2074
2073
|
* Build the /api/update route family.
|
|
2075
2074
|
* @param deps - fence + check/run seams.
|
|
@@ -2117,6 +2116,47 @@ function makeUpdateRoutes(deps) {
|
|
|
2117
2116
|
}];
|
|
2118
2117
|
}
|
|
2119
2118
|
//#endregion
|
|
2119
|
+
//#region src/mount-once.ts
|
|
2120
|
+
/**
|
|
2121
|
+
* Host single-instance guard shared by the plugin family. The family bundle
|
|
2122
|
+
* (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
2123
|
+
* the loader accepts a standalone install of the same package side by side;
|
|
2124
|
+
* without this guard the second instance would still re-register the same
|
|
2125
|
+
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
2126
|
+
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
2127
|
+
* lifetime of the first instance (the browser half is already deduped by
|
|
2128
|
+
* package name in the client module host).
|
|
2129
|
+
*
|
|
2130
|
+
* The registry rides a global symbol so two module instances of the same
|
|
2131
|
+
* package (npm copy vs repository link) still share one verdict. cordis
|
|
2132
|
+
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
2133
|
+
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
2134
|
+
*/
|
|
2135
|
+
const MOUNTED = Symbol.for("dsh-web-ui.mounted-plugins");
|
|
2136
|
+
function mountedSet() {
|
|
2137
|
+
const registry = globalThis;
|
|
2138
|
+
return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
2142
|
+
* The first mount registers normally and unmarks when its fiber disposes;
|
|
2143
|
+
* any later mount of the same package name is a no-op.
|
|
2144
|
+
* @param packageName - npm package identity shared by every install source.
|
|
2145
|
+
* @param fn - the original plugin apply.
|
|
2146
|
+
* @returns an apply of the same shape.
|
|
2147
|
+
*/
|
|
2148
|
+
function mountOnce(packageName, fn) {
|
|
2149
|
+
return ((...args) => {
|
|
2150
|
+
const mounted = mountedSet();
|
|
2151
|
+
if (mounted.has(packageName)) return;
|
|
2152
|
+
mounted.add(packageName);
|
|
2153
|
+
args[0]?.effect?.(() => () => {
|
|
2154
|
+
mounted.delete(packageName);
|
|
2155
|
+
});
|
|
2156
|
+
return fn(...args);
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
//#endregion
|
|
2120
2160
|
//#region src/index.ts
|
|
2121
2161
|
/**
|
|
2122
2162
|
* Mobile remote control for the dsh web GUI — host half. Mounts the pairing
|
|
@@ -2167,7 +2207,8 @@ const DEFAULTS = {
|
|
|
2167
2207
|
* @param ctx - host plugin context carrying webServer.
|
|
2168
2208
|
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
2169
2209
|
*/
|
|
2170
|
-
|
|
2210
|
+
const apply = mountOnce("@linxin666/dsh-remote-web-ui", applyImpl);
|
|
2211
|
+
function applyImpl(ctx, config) {
|
|
2171
2212
|
const resolved = {
|
|
2172
2213
|
tokenTtlMs: config?.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
2173
2214
|
offlineAfterMs: config?.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
package/lib/mobile.js
CHANGED
|
@@ -13429,7 +13429,17 @@ function foldEvents(events, existing) {
|
|
|
13429
13429
|
if (event.seq <= watermark) continue;
|
|
13430
13430
|
applyEvent(state, event);
|
|
13431
13431
|
}
|
|
13432
|
-
|
|
13432
|
+
const out = state.messages;
|
|
13433
|
+
let ordered = true;
|
|
13434
|
+
for (let index = 1; index < out.length; index += 1) {
|
|
13435
|
+
const prev = out[index - 1];
|
|
13436
|
+
const current = out[index];
|
|
13437
|
+
if (prev.seq > current.seq || prev.seq === current.seq && prev.id >= current.id) {
|
|
13438
|
+
ordered = false;
|
|
13439
|
+
break;
|
|
13440
|
+
}
|
|
13441
|
+
}
|
|
13442
|
+
return ordered ? out : [...out].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
|
|
13433
13443
|
}
|
|
13434
13444
|
//#endregion
|
|
13435
13445
|
//#region src/mobile/markdown.ts
|
|
@@ -13450,15 +13460,18 @@ function escapeHtml(text) {
|
|
|
13450
13460
|
}
|
|
13451
13461
|
/**
|
|
13452
13462
|
* Guard a raw link/image target against dangerous protocols. Returns the
|
|
13453
|
-
* (trimmed) raw string when safe, else null. Only http:, https:, mailto
|
|
13454
|
-
*
|
|
13455
|
-
* — javascript:, data:, vbscript:, etc. —
|
|
13456
|
-
*
|
|
13463
|
+
* (trimmed) raw string when safe, else null. Only http:, https:, mailto:,
|
|
13464
|
+
* fragment anchors (#...) and strictly relative paths are allowed; anything
|
|
13465
|
+
* with another scheme — javascript:, data:, vbscript:, etc. — or a
|
|
13466
|
+
* protocol-relative //host target (the browser resolves it against the
|
|
13467
|
+
* current scheme, reaching an arbitrary origin) is rejected so the value
|
|
13468
|
+
* never reaches dangerouslySetInnerHTML.
|
|
13457
13469
|
*/
|
|
13458
13470
|
function safeUrl(raw) {
|
|
13459
13471
|
const trimmed = raw.trim();
|
|
13460
13472
|
if (trimmed === "") return null;
|
|
13461
13473
|
if (trimmed.startsWith("#")) return trimmed;
|
|
13474
|
+
if (trimmed.startsWith("//")) return null;
|
|
13462
13475
|
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
|
|
13463
13476
|
if (scheme === null) return trimmed;
|
|
13464
13477
|
const name = scheme[1].toLowerCase();
|