dsh-multi-chat 0.5.2 → 0.6.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/package.json
CHANGED
|
@@ -242,27 +242,27 @@ window.__ModuleLoader__.load({
|
|
|
242
242
|
document.head.appendChild(tag);
|
|
243
243
|
}
|
|
244
244
|
var WallView_module_css_default = {
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"confirm": "ouhzTG_confirm",
|
|
248
|
-
"field": "ouhzTG_field",
|
|
245
|
+
"paneActions": "ouhzTG_paneActions",
|
|
246
|
+
"linkBar": "ouhzTG_linkBar",
|
|
249
247
|
"wall": "ouhzTG_wall",
|
|
250
|
-
"
|
|
248
|
+
"action": "ouhzTG_action",
|
|
249
|
+
"danger": "ouhzTG_danger",
|
|
250
|
+
"paneBody": "ouhzTG_paneBody",
|
|
251
|
+
"title": "ouhzTG_title",
|
|
251
252
|
"hint": "ouhzTG_hint",
|
|
253
|
+
"empty": "ouhzTG_empty",
|
|
254
|
+
"zoomed": "ouhzTG_zoomed",
|
|
255
|
+
"field": "ouhzTG_field",
|
|
256
|
+
"paneHead": "ouhzTG_paneHead",
|
|
252
257
|
"status": "ouhzTG_status",
|
|
253
258
|
"paneTitle": "ouhzTG_paneTitle",
|
|
254
|
-
"
|
|
255
|
-
"linkText": "ouhzTG_linkText",
|
|
256
|
-
"paneHead": "ouhzTG_paneHead",
|
|
257
|
-
"paneBody": "ouhzTG_paneBody",
|
|
258
|
-
"empty": "ouhzTG_empty",
|
|
259
|
+
"grid": "ouhzTG_grid",
|
|
259
260
|
"dot": "ouhzTG_dot",
|
|
260
|
-
"toolbar": "ouhzTG_toolbar",
|
|
261
|
-
"linkBar": "ouhzTG_linkBar",
|
|
262
|
-
"title": "ouhzTG_title",
|
|
263
261
|
"pane": "ouhzTG_pane",
|
|
264
|
-
"
|
|
265
|
-
"
|
|
262
|
+
"confirm": "ouhzTG_confirm",
|
|
263
|
+
"toolbar": "ouhzTG_toolbar",
|
|
264
|
+
"controls": "ouhzTG_controls",
|
|
265
|
+
"linkText": "ouhzTG_linkText"
|
|
266
266
|
};
|
|
267
267
|
//#endregion
|
|
268
268
|
//#region src/client/WallView.tsx
|
|
@@ -294,6 +294,27 @@ window.__ModuleLoader__.load({
|
|
|
294
294
|
/** Embed flag appended to every pane URL; such pages register no wall UI. */
|
|
295
295
|
const EMBED_FLAG = "multi-wall=embed";
|
|
296
296
|
/**
|
|
297
|
+
* Whether this wall is being viewed through the phone gateway (a non-loopback
|
|
298
|
+
* host) rather than on the machine running DSH. When remote, pane iframes must
|
|
299
|
+
* load through the gateway's `/gw/<port>` route — a phone's `127.0.0.1` points
|
|
300
|
+
* at the phone itself, not the host.
|
|
301
|
+
*/
|
|
302
|
+
function isRemoteViewer() {
|
|
303
|
+
const host = typeof window !== "undefined" ? window.location.hostname : "";
|
|
304
|
+
return host !== "localhost" && host !== "127.0.0.1" && host !== "::1";
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The iframe URL for a pane. Local viewers embed the loopback instance
|
|
308
|
+
* directly; remote (phone) viewers route through the gateway that is already
|
|
309
|
+
* serving this page, appending the port so the gateway proxies to it.
|
|
310
|
+
* @param port - target DSH instance port.
|
|
311
|
+
* @returns the pane URL.
|
|
312
|
+
*/
|
|
313
|
+
function paneUrl(port) {
|
|
314
|
+
if (isRemoteViewer()) return `${window.location.origin}/gw/${port}/?${EMBED_FLAG}`;
|
|
315
|
+
return `http://127.0.0.1:${port}/?${EMBED_FLAG}`;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
297
318
|
* One pane: header (port, liveness dot, zoom/refresh/open/stop/remove) plus
|
|
298
319
|
* the embedded original DSH UI.
|
|
299
320
|
*/
|
|
@@ -338,7 +359,7 @@ window.__ModuleLoader__.load({
|
|
|
338
359
|
className: WallView_module_css_default.action,
|
|
339
360
|
title: t("openTab"),
|
|
340
361
|
onClick: () => {
|
|
341
|
-
window.open(
|
|
362
|
+
window.open(paneUrl(port), "_blank");
|
|
342
363
|
},
|
|
343
364
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, { size: 14 })
|
|
344
365
|
}),
|
|
@@ -363,7 +384,7 @@ window.__ModuleLoader__.load({
|
|
|
363
384
|
className: WallView_module_css_default.paneBody,
|
|
364
385
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
365
386
|
title: `DSH :${port}`,
|
|
366
|
-
src:
|
|
387
|
+
src: paneUrl(port),
|
|
367
388
|
loading: "lazy"
|
|
368
389
|
})
|
|
369
390
|
})]
|
|
@@ -660,8 +681,8 @@ window.__ModuleLoader__.load({
|
|
|
660
681
|
document.head.appendChild(tag);
|
|
661
682
|
}
|
|
662
683
|
var WallToggle_module_css_default = {
|
|
663
|
-
"
|
|
664
|
-
"
|
|
684
|
+
"row": "bmjS6q_row",
|
|
685
|
+
"label": "bmjS6q_label"
|
|
665
686
|
};
|
|
666
687
|
//#endregion
|
|
667
688
|
//#region src/client/WallToggle.tsx
|
|
@@ -170,11 +170,13 @@ function injectPolyfill(body) {
|
|
|
170
170
|
* `127.0.0.1:<target-port>` so the DSH `/api` browser-trust fence sees a local
|
|
171
171
|
* request. Non-upgrade requests get `connection: close` (fresh connection per
|
|
172
172
|
* request keeps keep-alive from smuggling unrewritten Host headers); upgrades
|
|
173
|
-
* (WebSocket) keep `connection: Upgrade`.
|
|
173
|
+
* (WebSocket) keep `connection: Upgrade`. When `pathOverride` is set (a
|
|
174
|
+
* `/gw/<port>` route), the request line's path becomes the de-prefixed path.
|
|
174
175
|
*/
|
|
175
|
-
function rewriteHead(parsed, targetPort) {
|
|
176
|
+
function rewriteHead(parsed, targetPort, pathOverride) {
|
|
176
177
|
const upgrade = parsed.map.upgrade !== void 0;
|
|
177
|
-
const
|
|
178
|
+
const requestTarget = pathOverride !== void 0 ? pathOverride : parsed.target;
|
|
179
|
+
const lines = [`${parsed.method} ${requestTarget} ${parsed.version}`];
|
|
178
180
|
for (const [name, value] of parsed.headers) if (name === "host") lines.push(`host: 127.0.0.1:${targetPort}`);
|
|
179
181
|
else if (name === "origin") lines.push(`origin: http://127.0.0.1:${targetPort}`);
|
|
180
182
|
else if (name === "connection" || name === "proxy-connection") {} else lines.push(`${name}: ${value}`);
|
|
@@ -187,7 +189,8 @@ function rewriteHead(parsed, targetPort) {
|
|
|
187
189
|
* @returns a handle with the assigned port and token, and a close().
|
|
188
190
|
*/
|
|
189
191
|
function startGateway(options) {
|
|
190
|
-
const { targetPort, port: requestedPort, token, name, maxAgeHours = 12, log = () => {} } = options;
|
|
192
|
+
const { targetPort, port: requestedPort, token, name, maxAgeHours = 12, routedPorts, log = () => {} } = options;
|
|
193
|
+
const allowedRouted = new Set(routedPorts ?? []);
|
|
191
194
|
const hmac = (data) => createHmac("sha256", token).update(data).digest("hex");
|
|
192
195
|
const signSession = (exp) => {
|
|
193
196
|
const payload = Buffer.from(JSON.stringify({ exp })).toString("base64url");
|
|
@@ -412,6 +415,19 @@ connection: close\r
|
|
|
412
415
|
handleLogin(socket, parsed, buffer.subarray(parsed.headBytes));
|
|
413
416
|
return;
|
|
414
417
|
}
|
|
418
|
+
let routePort = null;
|
|
419
|
+
let routePath;
|
|
420
|
+
const routeMatch = /^\/gw\/(\d+)(\/.*)?$/.exec(path);
|
|
421
|
+
if (routeMatch !== null) {
|
|
422
|
+
const candidate = Number(routeMatch[1]);
|
|
423
|
+
if (!Number.isInteger(candidate) || candidate <= 0 || !allowedRouted.has(candidate)) {
|
|
424
|
+
respondPlain(socket, "403", "port not routable");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
routePort = candidate;
|
|
428
|
+
routePath = routeMatch[2] && routeMatch[2] !== "" ? routeMatch[2] : "/";
|
|
429
|
+
}
|
|
430
|
+
const effectivePort = routePort ?? targetPort;
|
|
415
431
|
const kind = authKind(parsed);
|
|
416
432
|
if (kind === "none") {
|
|
417
433
|
respondLogin(socket, name, path === "/" ? "/" : path);
|
|
@@ -421,19 +437,19 @@ connection: close\r
|
|
|
421
437
|
respondSessionRedirect(socket, parsed, `dsh_gw_session=${signSession(Date.now() + maxAgeHours * 36e5)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAgeHours * 3600}`);
|
|
422
438
|
return;
|
|
423
439
|
}
|
|
424
|
-
const target = connect(
|
|
440
|
+
const target = connect(effectivePort, "127.0.0.1");
|
|
425
441
|
const rest = buffer.subarray(parsed.headBytes);
|
|
426
442
|
let connected = false;
|
|
427
443
|
target.on("connect", () => {
|
|
428
444
|
connected = true;
|
|
429
|
-
target.write(rewriteHead(parsed,
|
|
445
|
+
target.write(rewriteHead(parsed, effectivePort, routePath));
|
|
430
446
|
if (rest.length > 0) target.write(rest);
|
|
431
447
|
proxying = true;
|
|
432
448
|
socket.pipe(target);
|
|
433
449
|
relayTarget(socket, target);
|
|
434
450
|
});
|
|
435
451
|
target.on("error", (error) => {
|
|
436
|
-
if (!connected) respondPlain(socket, "502", `cannot reach target 127.0.0.1:${
|
|
452
|
+
if (!connected) respondPlain(socket, "502", `cannot reach target 127.0.0.1:${effectivePort}: ${error.message}`);
|
|
437
453
|
socket.destroy();
|
|
438
454
|
});
|
|
439
455
|
socket.on("error", () => target.destroy());
|
|
@@ -817,7 +833,8 @@ function lanAddresses() {
|
|
|
817
833
|
if (virtual) continue;
|
|
818
834
|
candidates.push({
|
|
819
835
|
address: iface.address,
|
|
820
|
-
physical
|
|
836
|
+
physical,
|
|
837
|
+
virtual: false
|
|
821
838
|
});
|
|
822
839
|
}
|
|
823
840
|
}
|
|
@@ -960,11 +977,15 @@ function apply(ctx, config = {}) {
|
|
|
960
977
|
const token = config.gatewayToken && config.gatewayToken !== "" ? config.gatewayToken : randomBytes(6).toString("hex");
|
|
961
978
|
const gatewayPort = config.gatewayPort && config.gatewayPort !== 0 ? config.gatewayPort : port + 5e3;
|
|
962
979
|
gatewayTargetPort = port;
|
|
980
|
+
const routed = [];
|
|
981
|
+
for (let p = scanFrom; p <= scanTo; p++) routed.push(p);
|
|
982
|
+
for (const p of fixedPorts) if (!routed.includes(p)) routed.push(p);
|
|
963
983
|
return startGateway({
|
|
964
984
|
targetPort: port,
|
|
965
985
|
port: gatewayPort,
|
|
966
986
|
token,
|
|
967
987
|
name: "DSH",
|
|
988
|
+
routedPorts: routed,
|
|
968
989
|
log: (msg) => ctx.logger.info(`multi-wall gateway: ${msg}`)
|
|
969
990
|
}).then((handle) => {
|
|
970
991
|
gateway = handle;
|