@linxin666/dsh-remote-web-ui 0.1.18 → 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 +54 -11
- package/lib/client.js.map +1 -1
- package/lib/index.js +137 -96
- 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 +5 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +9 -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.d.ts.map +1 -1
- package/lib/types/client/index.js +20 -14
- package/lib/types/client/settings-form.d.ts +8 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +15 -1
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/gate.d.ts +4 -9
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -32
- 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/loopback.d.ts +25 -0
- package/lib/types/loopback.d.ts.map +1 -0
- package/lib/types/loopback.js +68 -0
- 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/client/RemoteSettingsCard.tsx +8 -0
- package/src/client/index.ts +13 -7
- package/src/client/settings-form.ts +15 -1
- package/src/gate.ts +5 -30
- package/src/http.ts +30 -0
- package/src/index.ts +4 -1
- package/src/loopback.ts +63 -0
- 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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Host single-instance guard shared by the plugin family. The family bundle
|
|
4
|
+
* (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
5
|
+
* the loader accepts a standalone install of the same package side by side;
|
|
6
|
+
* without this guard the second instance would still re-register the same
|
|
7
|
+
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
8
|
+
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
9
|
+
* lifetime of the first instance (the browser half is already deduped by
|
|
10
|
+
* package name in the client module host).
|
|
11
|
+
*
|
|
12
|
+
* The registry rides a global symbol so two module instances of the same
|
|
13
|
+
* package (npm copy vs repository link) still share one verdict. cordis
|
|
14
|
+
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
15
|
+
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
|
+
*/
|
|
17
|
+
const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins');
|
|
18
|
+
function mountedSet() {
|
|
19
|
+
const registry = globalThis;
|
|
20
|
+
return (registry[MOUNTED] ??= new Set());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
24
|
+
* The first mount registers normally and unmarks when its fiber disposes;
|
|
25
|
+
* any later mount of the same package name is a no-op.
|
|
26
|
+
* @param packageName - npm package identity shared by every install source.
|
|
27
|
+
* @param fn - the original plugin apply.
|
|
28
|
+
* @returns an apply of the same shape.
|
|
29
|
+
*/
|
|
30
|
+
export function mountOnce(packageName, fn) {
|
|
31
|
+
return ((...args) => {
|
|
32
|
+
const mounted = mountedSet();
|
|
33
|
+
if (mounted.has(packageName))
|
|
34
|
+
return;
|
|
35
|
+
mounted.add(packageName);
|
|
36
|
+
const ctx = args[0];
|
|
37
|
+
ctx?.effect?.(() => () => {
|
|
38
|
+
mounted.delete(packageName);
|
|
39
|
+
});
|
|
40
|
+
return fn(...args);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,uDAAuD;AACvD,MAAM,MAAM,YAAY;AACtB,mEAAmE;AACjE,cAAc;AAChB,oEAAoE;GAClE,SAAS;AACX,4DAA4D;GAC1D,SAAS;AACX,sDAAsD;GACpD,WAAW;AACb,6EAA6E;GAC3E,cAAc,CAAA;AAElB,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,QAAQ,EAAE,OAAO,CAAA;IACjB,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAA;IACV,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACxC,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,YAAY,CAAA;IACnB,yEAAyE;IACzE,YAAY,EAAE,OAAO,CAAA;IACrB,oEAAoE;IACpE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAA;IACtB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,sCAAsC;AACtC,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,CAAA;AAE3C,yEAAyE;AACzE,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C;;OAEG;
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -37,12 +37,12 @@ export declare function isTrustedApiRequest(request: IncomingMessage, trustedHos
|
|
|
37
37
|
export declare function publicHostOf(url: string | undefined): string | undefined;
|
|
38
38
|
/** Route paths (exact matches under /api). */
|
|
39
39
|
export declare const PAIR_PATHS: {
|
|
40
|
-
readonly issue:
|
|
41
|
-
readonly accept:
|
|
42
|
-
readonly stop:
|
|
43
|
-
readonly heartbeat:
|
|
44
|
-
readonly status:
|
|
45
|
-
readonly events:
|
|
40
|
+
readonly issue: '/api/pair/issue';
|
|
41
|
+
readonly accept: '/api/pair/accept';
|
|
42
|
+
readonly stop: '/api/pair/stop';
|
|
43
|
+
readonly heartbeat: '/api/pair/heartbeat';
|
|
44
|
+
readonly status: '/api/pair/status';
|
|
45
|
+
readonly events: '/api/pair/events';
|
|
46
46
|
};
|
|
47
47
|
/**
|
|
48
48
|
* /api/pair request payload contracts. Each POST endpoint validates its body
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;aACrB,KAAK,EAAE,iBAAiB;aACxB,MAAM,EAAE,kBAAkB;aAC1B,IAAI,EAAE,gBAAgB;aACtB,SAAS,EAAE,qBAAqB;aAChC,MAAM,EAAE,kBAAkB;aAC1B,MAAM,EAAE,kBAAkB;CAClB,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;IACH,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAepD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAUpC;IAED,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAoL3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* on every other /api request.
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import { UnknownLanAddressError } from
|
|
12
|
-
import { isLoopbackClient, readCookie } from
|
|
11
|
+
import { UnknownLanAddressError } from './pairing.js';
|
|
12
|
+
import { isLoopbackClient, readCookie } from './gate.js';
|
|
13
|
+
import { readBoundedJson, writeJson } from './http.js';
|
|
13
14
|
/**
|
|
14
15
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
15
16
|
* package's internal fence semantics (Host/Origin based, DNS-rebinding and
|
|
@@ -117,25 +118,10 @@ function parsePairPayload(schema, body) {
|
|
|
117
118
|
const result = schema.safeParse(body ?? {});
|
|
118
119
|
return result.success ? result.data : undefined;
|
|
119
120
|
}
|
|
120
|
-
/**
|
|
121
|
-
function writeJson(res, status, body) {
|
|
122
|
-
const payload = JSON.stringify(body);
|
|
123
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' });
|
|
124
|
-
res.end(payload);
|
|
125
|
-
}
|
|
126
|
-
/** Read a request body up to MAX_BODY_BYTES and parse it as JSON. */
|
|
121
|
+
/** Read a request body up to MAX_BODY_BYTES and parse it as JSON (undefined on failure). */
|
|
127
122
|
async function readJsonBody(req) {
|
|
128
|
-
const chunks = [];
|
|
129
|
-
let size = 0;
|
|
130
|
-
for await (const chunk of req) {
|
|
131
|
-
const buffer = chunk;
|
|
132
|
-
size += buffer.length;
|
|
133
|
-
if (size > MAX_BODY_BYTES)
|
|
134
|
-
return undefined;
|
|
135
|
-
chunks.push(buffer);
|
|
136
|
-
}
|
|
137
123
|
try {
|
|
138
|
-
const parsed =
|
|
124
|
+
const parsed = await readBoundedJson(req, MAX_BODY_BYTES);
|
|
139
125
|
return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
|
|
140
126
|
}
|
|
141
127
|
catch {
|
|
@@ -216,6 +202,15 @@ export function makeRoutes(deps) {
|
|
|
216
202
|
const rateLimitAccept = (req) => {
|
|
217
203
|
const ip = req.socket?.remoteAddress ?? 'unknown';
|
|
218
204
|
const nowMs = Date.now();
|
|
205
|
+
// The map lives as long as the plugin: prune expired windows once the
|
|
206
|
+
// table grows past a modest size so distinct source IPs (LAN clients,
|
|
207
|
+
// brute-force scans) cannot accumulate forever.
|
|
208
|
+
if (acceptAttempts.size > 256) {
|
|
209
|
+
for (const [key, attempt] of acceptAttempts) {
|
|
210
|
+
if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS)
|
|
211
|
+
acceptAttempts.delete(key);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
219
214
|
const entry = acceptAttempts.get(ip);
|
|
220
215
|
if (entry === undefined || nowMs - entry.windowStart > ACCEPT_WINDOW_MS) {
|
|
221
216
|
acceptAttempts.set(ip, { count: 1, windowStart: nowMs });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAkBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IAEvE;;OAEG;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAkBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IAEvE;;OAEG;IACH,YAAY,OAAO,GAAE,oBAAyB,EAO7C;IAED,gCAAgC;IAChC,IAAI,IAAI,IAAI,UAAU,CAMrB;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAO7B;IAED,uDAAuD;IACvD,IAAI,IAAI,IAAI,CAKX;IAED,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAEd;IAED,4EAA4E;IAC5E,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAGjD;IAED,uCAAuC;IACvC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAGxD;IAED,OAAO,CAAC,OAAO;IAoCf,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,IAAI;IAqBZ,iFAAiF;IACjF,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,QAAQ;CAWjB"}
|
|
@@ -9,8 +9,8 @@ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
|
9
9
|
import type { UpdateRunResult, UpdateStatus } from './update.ts';
|
|
10
10
|
/** Route paths (exact matches under /api). */
|
|
11
11
|
export declare const UPDATE_PATHS: {
|
|
12
|
-
readonly status:
|
|
13
|
-
readonly run:
|
|
12
|
+
readonly status: '/api/update/status';
|
|
13
|
+
readonly run: '/api/update/run';
|
|
14
14
|
};
|
|
15
15
|
/** Route-family dependencies (test seam). */
|
|
16
16
|
export interface UpdateRoutesDeps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-routes.d.ts","sourceRoot":"","sources":["../../src/update-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"update-routes.d.ts","sourceRoot":"","sources":["../../src/update-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGhE,8CAA8C;AAC9C,eAAO,MAAM,YAAY;aACvB,MAAM,EAAE,oBAAoB;aAC5B,GAAG,EAAE,iBAAiB;CACd,CAAA;AAEV,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IACxC,yDAAyD;IACzD,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC,CAAA;IAC9B,sDAAsD;IACtD,GAAG,IAAI,OAAO,CAAC,eAAe,CAAC,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,QAAQ,EAAE,CA+BnE"}
|
|
@@ -4,17 +4,12 @@
|
|
|
4
4
|
* pnpm install inside the owning profile, so it must never be reachable
|
|
5
5
|
* from a LAN/phone origin.
|
|
6
6
|
*/
|
|
7
|
+
import { writeJson } from './http.js';
|
|
7
8
|
/** Route paths (exact matches under /api). */
|
|
8
9
|
export const UPDATE_PATHS = {
|
|
9
10
|
status: '/api/update/status',
|
|
10
11
|
run: '/api/update/run',
|
|
11
12
|
};
|
|
12
|
-
/** One JSON response. */
|
|
13
|
-
function writeJson(res, status, body) {
|
|
14
|
-
const payload = JSON.stringify(body);
|
|
15
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' });
|
|
16
|
-
res.end(payload);
|
|
17
|
-
}
|
|
18
13
|
/**
|
|
19
14
|
* Build the /api/update route family.
|
|
20
15
|
* @param deps - fence + check/run seams.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,oEAAoE;AACpE,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,8BAA8B,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,YAAY,iCAAiC,CAAA;AAQ1D,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CASlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5D;AAYD,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAahF;AAED,6DAA6D;AAC7D,KAAK,cAAc,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAE9D,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG1D;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,iGAAiG;IACjG,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;IAChC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAA;IACjB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9C,oEAAoE;IACpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACjD,MAAM,GAAG,SAAS,CAUpB;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpC,YAAY,GAAG;IAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAAA;CAAE,CAiBhD;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAQhF;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,EAC9E,SAAS,SAAS,GACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiB7B;AAqBD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAmD/E;AAED,4DAA4D;AAC5D,MAAM,MAAM,eAAe;AACzB,2BAA2B;AACzB,cAAc;AAChB,6CAA6C;GAC3C,SAAS;AACX,2CAA2C;GACzC,WAAW;AACb,iEAAiE;GAC/D,MAAM;AACR,4BAA4B;GAC1B,aAAa;AACf,6DAA6D;GAC3D,OAAO;AACT,iFAAiF;GAC/E,eAAe,CAAA;AAEnB,gCAAgC;AAChC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAA;IACX,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;CAC3B;AAuBD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CA8HvE;AAED,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB;IACpC,iDAAiD;IACjD,GAAG,EAAE,aAAa,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,eAAe,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAgE7F"}
|
package/lib/types/update.js
CHANGED
|
@@ -268,10 +268,13 @@ export async function checkUpdates(deps) {
|
|
|
268
268
|
return { mode: 'link', packages: [], outdated: false };
|
|
269
269
|
}
|
|
270
270
|
const names = [anchor, ...familyChildren(manifest)];
|
|
271
|
+
// The registry probes are independent: run them together instead of
|
|
272
|
+
// serializing up to N x 10s of registry latency behind one status call.
|
|
273
|
+
const latestList = await Promise.all(names.map(name => deps.fetchLatest(name)));
|
|
271
274
|
const packages = [];
|
|
272
275
|
let probeFailures = 0;
|
|
273
|
-
|
|
274
|
-
const latest =
|
|
276
|
+
names.forEach((name, index) => {
|
|
277
|
+
const latest = latestList[index];
|
|
275
278
|
if (latest === undefined)
|
|
276
279
|
probeFailures++;
|
|
277
280
|
const current = readInstalledVersion(deps.resolve, name);
|
|
@@ -281,7 +284,7 @@ export async function checkUpdates(deps) {
|
|
|
281
284
|
latest,
|
|
282
285
|
outdated: latest !== undefined && latest !== current && compareVersions(latest, current) > 0,
|
|
283
286
|
});
|
|
284
|
-
}
|
|
287
|
+
});
|
|
285
288
|
// Registry unreachable: every probe failed — report the outage distinctly
|
|
286
289
|
// instead of a misleading "all up to date" (the panel needs the reason).
|
|
287
290
|
const error = probeFailures === names.length && names.length > 0 ? 'registry-unreachable' : undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -20,8 +20,18 @@ import css from './remote.module.css'
|
|
|
20
20
|
/** Entry props: the sidebar column state + the standard locale seat. */
|
|
21
21
|
export type RemoteEntryProps = PropsRuntime<'sidebar.remote'> & PropsLocale<'remote'>
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Apply one status frame onto the current state: the ready state mirrors
|
|
25
|
+
* the full phase/device picture, while the lan-required banner only keeps
|
|
26
|
+
* the auto-tunnel frame (the signal for the running re-issue).
|
|
27
|
+
*/
|
|
24
28
|
function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
|
|
29
|
+
if (state.kind === 'lan-required') {
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
...(frame.tunnel !== undefined ? { tunnel: frame.tunnel } : {}),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
25
35
|
if (state.kind !== 'ready') return state
|
|
26
36
|
return {
|
|
27
37
|
...state,
|
|
@@ -40,6 +50,11 @@ function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
|
|
|
40
50
|
export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
41
51
|
const [open, setOpen] = useState(false)
|
|
42
52
|
const [state, setState] = useState<PanelState>({ kind: 'lan-required' })
|
|
53
|
+
// Latest-state mirror for the EventSource callback: transition detection
|
|
54
|
+
// must live outside setState updaters (updaters may run twice and must be
|
|
55
|
+
// pure), so mint decisions read this ref instead.
|
|
56
|
+
const stateRef = useRef(state)
|
|
57
|
+
useEffect(() => { stateRef.current = state }, [state])
|
|
43
58
|
const [copied, setCopied] = useState(false)
|
|
44
59
|
const eventSource = useRef<EventSource | undefined>(undefined)
|
|
45
60
|
|
|
@@ -93,17 +108,34 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
93
108
|
const next = await mint()
|
|
94
109
|
setState(next)
|
|
95
110
|
// Live status: the desktop panel mirrors the pairing service state. The
|
|
96
|
-
// stream
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
111
|
+
// stream makes sense in the ready state and on the lan-required banner —
|
|
112
|
+
// there the auto-tunnel may still be starting, and following its frames
|
|
113
|
+
// lets the panel re-issue once it runs. The loopback-required and
|
|
114
|
+
// unreachable origins are fenced out of the events endpoint, so opening
|
|
115
|
+
// it there would just start a doomed reconnect loop.
|
|
116
|
+
if (next.kind !== 'ready' && next.kind !== 'lan-required') return
|
|
100
117
|
const source = new EventSource('/api/pair/events')
|
|
101
118
|
eventSource.current = source
|
|
102
119
|
source.onmessage = (event) => {
|
|
103
120
|
try {
|
|
104
121
|
const frame = JSON.parse(event.data as string) as PairStateFrame
|
|
105
122
|
if (frame.type !== 'state') return
|
|
106
|
-
|
|
123
|
+
// The auto-tunnel crossed into running while the panel sat on the
|
|
124
|
+
// lan-required banner: re-issue so the server hands back a ready QR
|
|
125
|
+
// built on the public base (only on the transition into running, to
|
|
126
|
+
// avoid mint storms). Detected on the ref, outside the updater: an
|
|
127
|
+
// updater may be invoked twice and must stay pure, so mint() cannot
|
|
128
|
+
// run inside it.
|
|
129
|
+
const previous = stateRef.current
|
|
130
|
+
if (
|
|
131
|
+
previous.kind === 'lan-required'
|
|
132
|
+
&& frame.tunnel?.state === 'running'
|
|
133
|
+
&& previous.tunnel?.state !== 'running'
|
|
134
|
+
) {
|
|
135
|
+
void mint().then(setState)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
setState(current => mergeFrame(current, frame))
|
|
107
139
|
} catch {
|
|
108
140
|
// Malformed frames are dropped; the snapshot on open is authoritative.
|
|
109
141
|
}
|
|
@@ -16,7 +16,7 @@ import css from './remote.module.css'
|
|
|
16
16
|
|
|
17
17
|
/** The panel's view state, owned by the entry component. */
|
|
18
18
|
export type PanelState =
|
|
19
|
-
| { kind: 'lan-required' }
|
|
19
|
+
| { kind: 'lan-required'; tunnel?: TunnelStatusFrame }
|
|
20
20
|
| { kind: 'loopback-required' }
|
|
21
21
|
| { kind: 'unreachable' }
|
|
22
22
|
| {
|
|
@@ -104,6 +104,14 @@ export class RemoteSettingsCardController {
|
|
|
104
104
|
inject(): RemoteSettingsCardFace {
|
|
105
105
|
return { hooks: { remoteSettingsCard: this.store }, ...this.form.actions() }
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Release the card's scope subscription and bound stores; the slot
|
|
110
|
+
* disposer calls this on teardown.
|
|
111
|
+
*/
|
|
112
|
+
dispose(): void {
|
|
113
|
+
this.form.dispose()
|
|
114
|
+
}
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
/** Props the renderer binds for the remote-control card. */
|
package/src/client/index.ts
CHANGED
|
@@ -157,13 +157,19 @@ export function apply(ctx: ClientContext): void {
|
|
|
157
157
|
// Plugin configuration card: one staged form over the `remote-web-ui`
|
|
158
158
|
// settings namespace, contributed to the Web UI plugin group.
|
|
159
159
|
const remoteSettings = new RemoteSettingsCardController(settingsScope)
|
|
160
|
-
ctx.slots.inject('web-ui.plugin.item', () =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
ctx.slots.inject('web-ui.plugin.item', () => {
|
|
161
|
+
const unregister = ctx.slots.register({
|
|
162
|
+
name: 'web-ui.plugin.item',
|
|
163
|
+
id: 'remote-web-ui',
|
|
164
|
+
order: 90,
|
|
165
|
+
locale: NS,
|
|
166
|
+
inject: () => remoteSettings.inject(),
|
|
167
|
+
}, RemoteSettingsCard)
|
|
168
|
+
return () => {
|
|
169
|
+
remoteSettings.dispose()
|
|
170
|
+
unregister()
|
|
171
|
+
}
|
|
172
|
+
})
|
|
167
173
|
|
|
168
174
|
// Phone-side boot flow + heartbeats. Loopback pages (the desktop) never
|
|
169
175
|
// heartbeat; the server ignores unpaired heartbeats anyway. Both run only
|
|
@@ -234,6 +234,9 @@ export class CardForm<T> {
|
|
|
234
234
|
private readonly specs: Map<string, FieldSpec>
|
|
235
235
|
private readonly staged = new Map<string, StagedEdit>()
|
|
236
236
|
private readonly listeners = new Set<() => void>()
|
|
237
|
+
/** The scope subscription installed in the constructor; released by dispose(). */
|
|
238
|
+
private readonly disposeScope: () => void
|
|
239
|
+
private disposed = false
|
|
237
240
|
private saving = false
|
|
238
241
|
private failed = false
|
|
239
242
|
private failedReason: string | undefined
|
|
@@ -244,7 +247,18 @@ export class CardForm<T> {
|
|
|
244
247
|
specs: FieldSpec[],
|
|
245
248
|
) {
|
|
246
249
|
this.specs = new Map(specs.map(spec => [spec.field, spec]))
|
|
247
|
-
scope.subscribe(() => { this.publish() })
|
|
250
|
+
this.disposeScope = scope.subscribe(() => { this.publish() })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Release the scope subscription and every bound store listener. The card
|
|
255
|
+
* must call this on teardown; later calls are no-ops.
|
|
256
|
+
*/
|
|
257
|
+
dispose(): void {
|
|
258
|
+
if (this.disposed) return
|
|
259
|
+
this.disposed = true
|
|
260
|
+
this.disposeScope()
|
|
261
|
+
this.listeners.clear()
|
|
248
262
|
}
|
|
249
263
|
|
|
250
264
|
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
package/src/gate.ts
CHANGED
|
@@ -13,41 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
import type { IncomingMessage } from 'node:http'
|
|
15
15
|
import type { PairingService } from './pairing.ts'
|
|
16
|
+
import { isLoopbackAddress, isLoopbackHostname } from './loopback.ts'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* IPv6 loopback, any IPv4 address
|
|
21
|
-
*
|
|
22
|
-
* the connection plugin, and external host plugins only need the
|
|
23
|
-
* classification, not the whole trust decision.
|
|
19
|
+
* Loopback classification for the desktop client. The predicates now live in
|
|
20
|
+
* the shared synced copy (shared/host/loopback.ts, mirrored to ./loopback.ts
|
|
21
|
+
* by scripts/sync-shared.mjs): localhost, IPv6 loopback, and any IPv4 address
|
|
22
|
+
* in 127/8.
|
|
24
23
|
* @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
|
|
25
24
|
* @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
|
|
26
25
|
*/
|
|
27
|
-
export function isLoopbackHostname(hostname: string): boolean {
|
|
28
|
-
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
29
|
-
const parts = hostname.split('.')
|
|
30
|
-
return parts.length === 4
|
|
31
|
-
&& parts[0] === '127'
|
|
32
|
-
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
36
|
-
export function isLoopbackAddress(address: string | undefined): boolean {
|
|
37
|
-
if (address === undefined) return false
|
|
38
|
-
const normalized = address.toLowerCase()
|
|
39
|
-
if (normalized === '::1') return true
|
|
40
|
-
if (normalized.startsWith('::ffff:')) return isIPv4Loopback(normalized.slice('::ffff:'.length))
|
|
41
|
-
return isIPv4Loopback(normalized)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
45
|
-
function isIPv4Loopback(v4: string): boolean {
|
|
46
|
-
const parts = v4.split('.')
|
|
47
|
-
return parts.length === 4
|
|
48
|
-
&& parts[0] === '127'
|
|
49
|
-
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
50
|
-
}
|
|
51
26
|
|
|
52
27
|
/**
|
|
53
28
|
* Read one cookie value from a Cookie header.
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP helpers for the route families: one JSON writer and one
|
|
3
|
+
* bounded JSON body reader. Previously copy-pasted across routes.ts,
|
|
4
|
+
* update-routes.ts, and mobile-api.ts with drifting failure contracts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
|
+
|
|
9
|
+
/** One JSON response. */
|
|
10
|
+
export function writeJson(res: ServerResponse, status: number, body: unknown): void {
|
|
11
|
+
const payload = JSON.stringify(body)
|
|
12
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' })
|
|
13
|
+
res.end(payload)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read a request body up to maxBytes and parse it as JSON.
|
|
18
|
+
* @throws 'body too large' beyond the cap, or the JSON.parse error.
|
|
19
|
+
*/
|
|
20
|
+
export async function readBoundedJson(req: IncomingMessage, maxBytes: number): Promise<unknown> {
|
|
21
|
+
const chunks: Buffer[] = []
|
|
22
|
+
let size = 0
|
|
23
|
+
for await (const chunk of req) {
|
|
24
|
+
const buffer = chunk as Buffer
|
|
25
|
+
size += buffer.length
|
|
26
|
+
if (size > maxBytes) throw new Error('body too large')
|
|
27
|
+
chunks.push(buffer)
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type UpdateRunResult,
|
|
32
32
|
} from './update.ts'
|
|
33
33
|
import { makeUpdateRoutes } from './update-routes.ts'
|
|
34
|
+
import { mountOnce } from './mount-once.ts'
|
|
34
35
|
|
|
35
36
|
declare module '@deepseek-ai/cordis' {
|
|
36
37
|
interface Events {
|
|
@@ -147,7 +148,9 @@ const DEFAULTS: ResolvedConfig = {
|
|
|
147
148
|
* @param ctx - host plugin context carrying webServer.
|
|
148
149
|
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
149
150
|
*/
|
|
150
|
-
export
|
|
151
|
+
export const apply = mountOnce('@linxin666/dsh-remote-web-ui', applyImpl)
|
|
152
|
+
|
|
153
|
+
function applyImpl(ctx: Context, config?: Config): void {
|
|
151
154
|
const resolved: ResolvedConfig = {
|
|
152
155
|
tokenTtlMs: config?.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
153
156
|
offlineAfterMs: config?.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
package/src/loopback.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/loopback.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Loopback trust fence shared by the host route families: socket address,
|
|
4
|
+
* Host header, and browser same-origin markers. Packages receive this file as
|
|
5
|
+
* a generated copy via scripts/sync-shared.mjs; edit the shared source and
|
|
6
|
+
* re-run the sync instead of editing a copy.
|
|
7
|
+
*
|
|
8
|
+
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
9
|
+
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
10
|
+
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
11
|
+
* @module dsh-web-ui-shared/host/loopback
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { IncomingMessage } from 'node:http'
|
|
15
|
+
|
|
16
|
+
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
17
|
+
export function isIPv4Loopback(v4: string): boolean {
|
|
18
|
+
const parts = v4.split('.')
|
|
19
|
+
return parts.length === 4
|
|
20
|
+
&& parts[0] === '127'
|
|
21
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
25
|
+
export function isLoopbackAddress(address: string | undefined): boolean {
|
|
26
|
+
if (address === undefined) return false
|
|
27
|
+
const normalized = address.toLowerCase()
|
|
28
|
+
if (normalized === '::1') return true
|
|
29
|
+
if (normalized.startsWith('::ffff:')) return isIPv4Loopback(normalized.slice('::ffff:'.length))
|
|
30
|
+
return isIPv4Loopback(normalized)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
|
|
34
|
+
export function isLoopbackHostname(hostname: string): boolean {
|
|
35
|
+
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
36
|
+
return isIPv4Loopback(hostname)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request-level trust fence: a loopback socket address AND a loopback Host
|
|
41
|
+
* header, plus browser same-origin markers. The socket address is
|
|
42
|
+
* authoritative; X-Forwarded-For is never trusted.
|
|
43
|
+
*/
|
|
44
|
+
export function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
45
|
+
if (!isLoopbackAddress(request.socket.remoteAddress)) return false
|
|
46
|
+
const host = request.headers.host
|
|
47
|
+
if (typeof host !== 'string') return false
|
|
48
|
+
let hostUrl: URL
|
|
49
|
+
try {
|
|
50
|
+
hostUrl = new URL('http://' + host)
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
if (!isLoopbackHostname(hostUrl.hostname)) return false
|
|
55
|
+
if (request.headers['sec-fetch-site'] === 'cross-site') return false
|
|
56
|
+
const origin = request.headers.origin
|
|
57
|
+
if (origin === undefined) return true
|
|
58
|
+
try {
|
|
59
|
+
return new URL(origin).host === hostUrl.host
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -22,6 +22,11 @@ describe('safeUrl', () => {
|
|
|
22
22
|
expect(safeUrl('')).toBeNull()
|
|
23
23
|
expect(safeUrl(' ')).toBeNull()
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('rejects protocol-relative targets that escape the origin', () => {
|
|
27
|
+
expect(safeUrl('//attacker.example/x')).toBeNull()
|
|
28
|
+
expect(safeUrl(' //attacker.example/track.png')).toBeNull()
|
|
29
|
+
})
|
|
25
30
|
})
|
|
26
31
|
|
|
27
32
|
describe('renderInline', () => {
|
package/src/mobile/markdown.ts
CHANGED
|
@@ -22,15 +22,18 @@ export function escapeHtml(text: string): string {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Guard a raw link/image target against dangerous protocols. Returns the
|
|
25
|
-
* (trimmed) raw string when safe, else null. Only http:, https:, mailto
|
|
26
|
-
*
|
|
27
|
-
* — javascript:, data:, vbscript:, etc. —
|
|
28
|
-
*
|
|
25
|
+
* (trimmed) raw string when safe, else null. Only http:, https:, mailto:,
|
|
26
|
+
* fragment anchors (#...) and strictly relative paths are allowed; anything
|
|
27
|
+
* with another scheme — javascript:, data:, vbscript:, etc. — or a
|
|
28
|
+
* protocol-relative //host target (the browser resolves it against the
|
|
29
|
+
* current scheme, reaching an arbitrary origin) is rejected so the value
|
|
30
|
+
* never reaches dangerouslySetInnerHTML.
|
|
29
31
|
*/
|
|
30
32
|
export function safeUrl(raw: string): string | null {
|
|
31
33
|
const trimmed = raw.trim()
|
|
32
34
|
if (trimmed === '') return null
|
|
33
35
|
if (trimmed.startsWith('#')) return trimmed
|
|
36
|
+
if (trimmed.startsWith('//')) return null
|
|
34
37
|
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)
|
|
35
38
|
if (scheme === null) return trimmed
|
|
36
39
|
const name = scheme[1].toLowerCase()
|
package/src/mobile/messages.ts
CHANGED
|
@@ -615,5 +615,18 @@ export function foldEvents(events: readonly WireEvent[], existing?: readonly Ren
|
|
|
615
615
|
if (event.seq <= watermark) continue
|
|
616
616
|
applyEvent(state, event)
|
|
617
617
|
}
|
|
618
|
-
|
|
618
|
+
// Incremental tails (the live-stream hot path) are appended in seq order
|
|
619
|
+
// or updated in place, so the list is usually ordered already — verify in
|
|
620
|
+
// O(n) and skip the O(n log n) re-sort when it is.
|
|
621
|
+
const out = state.messages
|
|
622
|
+
let ordered = true
|
|
623
|
+
for (let index = 1; index < out.length; index += 1) {
|
|
624
|
+
const prev = out[index - 1]!
|
|
625
|
+
const current = out[index]!
|
|
626
|
+
if (prev.seq > current.seq || (prev.seq === current.seq && prev.id >= current.id)) {
|
|
627
|
+
ordered = false
|
|
628
|
+
break
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return ordered ? out : [...out].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1))
|
|
619
632
|
}
|