@linxin666/dsh-remote-web-ui 0.4.5 → 0.4.7
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 +13 -13
- package/README.zh.md +13 -13
- package/lib/client.js +25 -92
- package/lib/client.js.map +1 -1
- package/lib/index.js +408 -53
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +3 -15
- package/lib/types/client/RemotePanel.d.ts +1 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +7 -4
- package/lib/types/client/locales.d.ts +4 -8
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +30 -3
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
- package/lib/types/client/plugin-card-seat.js +2 -0
- package/lib/types/client/settings-entry-form.d.ts.map +1 -1
- package/lib/types/client/settings-entry-form.js +0 -1
- package/lib/types/detached-work.d.ts +7 -7
- package/lib/types/detached-work.d.ts.map +1 -1
- package/lib/types/detached-work.js +9 -8
- package/lib/types/index.d.ts +14 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +67 -17
- package/lib/types/lan-source.d.ts +65 -0
- package/lib/types/lan-source.d.ts.map +1 -0
- package/lib/types/lan-source.js +113 -0
- package/lib/types/lan.d.ts +49 -3
- package/lib/types/lan.d.ts.map +1 -1
- package/lib/types/lan.js +120 -6
- package/lib/types/remote-api.d.ts +7 -2
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +17 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +4 -1
- package/lib/types/routes.d.ts +71 -9
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +157 -27
- package/lib/types/tunnel-plan.d.ts +8 -2
- package/lib/types/tunnel-plan.d.ts.map +1 -1
- package/lib/types/tunnel-plan.js +9 -1
- package/package.json +20 -25
- package/src/client/RemoteEntry.tsx +2 -18
- package/src/client/RemotePanel.tsx +9 -43
- package/src/client/locales.ts +4 -8
- package/src/client/mobile-adapt.ts +30 -3
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/remote.module.css +2 -48
- package/src/client/settings-entry-form.ts +0 -1
- package/src/detached-work.ts +9 -8
- package/src/index.ts +77 -17
- package/src/lan-source.ts +132 -0
- package/src/lan.ts +126 -6
- package/src/remote-api.ts +24 -4
- package/src/remote-channel-boot.ts +4 -1
- package/src/routes.ts +178 -27
- package/src/tunnel-plan.ts +9 -2
- package/lib/invariant.js +0 -21
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.d.ts.map +0 -1
- package/lib/types/invariant.js +0 -23
- package/src/invariant.ts +0 -32
|
@@ -200,7 +200,7 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
|
200
200
|
}
|
|
201
201
|
/** Boot-watchdog latch: one self-reload per session while the boot is broken. */
|
|
202
202
|
export const BOOT_WATCHDOG_KEY = 'dsh-remote-boot-reload';
|
|
203
|
-
/**
|
|
203
|
+
/** Post-load wait for the app's first conversation surface before the reload. */
|
|
204
204
|
const BOOT_WATCHDOG_WAIT_MS = 15_000;
|
|
205
205
|
/** Watchdog poll cadence. */
|
|
206
206
|
const BOOT_WATCHDOG_POLL_MS = 1_000;
|
|
@@ -221,6 +221,9 @@ export function buildBootWatchdogScript(waitMs = BOOT_WATCHDOG_WAIT_MS, pollMs =
|
|
|
221
221
|
';function wBoot(){try{return !!(w.document&&w.document.querySelector&&(w.document.querySelector("[data-conversation-scroll]")||w.document.querySelector("[data-slot=\\"conversation\\"]")))}catch(e){return false}}' +
|
|
222
222
|
'function wTick(n){try{' +
|
|
223
223
|
'if(wBoot()){try{w.sessionStorage.removeItem(' + key + ')}catch(e){}return}' +
|
|
224
|
+
// Parse-time installation must not spend the recovery budget downloading
|
|
225
|
+
// the initial scripts/styles over a slow remote link (issue #1845).
|
|
226
|
+
'if(!w.document||w.document.readyState!=="complete"){w.setTimeout(function(){wTick(0)},' + pollMs + ');return}' +
|
|
224
227
|
'if(n<' + waitMs + '){w.setTimeout(function(){wTick(n+' + pollMs + ')},' + pollMs + ');return}' +
|
|
225
228
|
'var done=false;try{done=w.sessionStorage.getItem(' + key + ')==="1"}catch(e){}' +
|
|
226
229
|
'if(done)return;' +
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
|
24
24
|
import { z } from 'zod';
|
|
25
25
|
import { type PairingService, type PairingSnapshot } from './pairing.ts';
|
|
26
26
|
import { type PairGrantStore } from './pair-grant.ts';
|
|
27
|
+
import { type LanSourceHop } from './lan-source.ts';
|
|
27
28
|
/**
|
|
28
29
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
29
30
|
* package's internal fence semantics (Host/Origin based, DNS-rebinding and
|
|
@@ -119,11 +120,18 @@ export declare function publicHostOf(url: string | undefined): string | undefine
|
|
|
119
120
|
* The cookieless device credential: pass the device id from the /pair-app
|
|
120
121
|
* URL into sessionStorage (and localStorage for tab reloads) before any app
|
|
121
122
|
* script runs - same key the boot patch and the channel gate read. The
|
|
122
|
-
* replaceState
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
123
|
+
* replaceState drops the one-time grant from the address bar and leaves the
|
|
124
|
+
* SPA on a path this plugin answers again; which path depends on the origin.
|
|
125
|
+
*
|
|
126
|
+
* A secure context registers the reopen service worker in the same breath,
|
|
127
|
+
* and that worker owns navigations to '/', so the SPA keeps its canonical
|
|
128
|
+
* root. A plain-HTTP LAN origin is never a secure context, so the worker
|
|
129
|
+
* cannot exist there and '/' falls through to the harness index gate the
|
|
130
|
+
* cookieless flow can never satisfy (issue #1867). The address bar therefore
|
|
131
|
+
* keeps the plugin-owned landing path, which re-validates the device
|
|
132
|
+
* credential on every reload and serves the bilingual re-pair page once the
|
|
133
|
+
* pairing is gone. Both targets are bare paths: the one-time grant never
|
|
134
|
+
* survives the rewrite, and no credential is ever left in the URL.
|
|
127
135
|
*/
|
|
128
136
|
export declare const APP_DEVICE_STORAGE_KEY = "dsh-remote-device";
|
|
129
137
|
export declare function appShellCaptureScript(deviceId: string): string;
|
|
@@ -134,8 +142,18 @@ export declare function appShellCaptureScript(deviceId: string): string;
|
|
|
134
142
|
* hook only when this server-served marker is already set (see
|
|
135
143
|
* remote-channel-boot.ts), so host mode is granted by the device-gated landing
|
|
136
144
|
* instead of being asserted from the origin.
|
|
145
|
+
*
|
|
146
|
+
* A landing reached on a base other than the machine's own LAN origin also
|
|
147
|
+
* carries the source-decision script (lan-source.ts): the phone decides at
|
|
148
|
+
* connect time whether a LAN candidate answers, and switches its origin when
|
|
149
|
+
* one does. It runs after the capture script so the granted page is fully
|
|
150
|
+
* usable before the probe's outcome can move it.
|
|
151
|
+
* @param html - the official index document.
|
|
152
|
+
* @param deviceId - the device session this landing is being served for.
|
|
153
|
+
* @param hop - the LAN candidates to probe, when a switch is possible.
|
|
154
|
+
* @returns the patched document.
|
|
137
155
|
*/
|
|
138
|
-
export declare function patchAppShell(html: string, deviceId: string): string;
|
|
156
|
+
export declare function patchAppShell(html: string, deviceId: string, hop?: LanSourceHop): string;
|
|
139
157
|
/**
|
|
140
158
|
* The reopen service worker served at PAIR_PATHS.appServiceWorker. A paired
|
|
141
159
|
* phone comes back to `/` from history, bookmarks, or tab restore; the
|
|
@@ -144,9 +162,10 @@ export declare function patchAppShell(html: string, deviceId: string): string;
|
|
|
144
162
|
* worker owns navigations to `/` instead: network-first through /pair-app
|
|
145
163
|
* (which validates the device cookie and refreshes its presence), the
|
|
146
164
|
* cached shell for offline opens, and a pass-through of the original
|
|
147
|
-
* request when the plugin no longer answers.
|
|
148
|
-
* secure
|
|
149
|
-
*
|
|
165
|
+
* request when the plugin no longer answers. A plain-HTTP LAN origin is
|
|
166
|
+
* never a secure context, so the worker cannot exist there; the capture
|
|
167
|
+
* script keeps the plugin-owned landing path in the address bar instead,
|
|
168
|
+
* and that landing re-validates the device credential on every reload.
|
|
150
169
|
*
|
|
151
170
|
* Kept as a plain string (same pattern as the capture script): it must load
|
|
152
171
|
* with no build step, run on every JS engine that ships service workers,
|
|
@@ -168,6 +187,12 @@ export declare const PAIR_PATHS: {
|
|
|
168
187
|
readonly acceptPage: "/pair-accept";
|
|
169
188
|
/** The cookieless app landing: serves the official shell for a paired device. */
|
|
170
189
|
readonly appPage: "/pair-app";
|
|
190
|
+
/**
|
|
191
|
+
* The landing page's LAN reachability beacon (lan-source.ts): an inert
|
|
192
|
+
* no-cors GET target, so a probe proves the origin answers without touching
|
|
193
|
+
* a credential or any state.
|
|
194
|
+
*/
|
|
195
|
+
readonly ping: "/pair-ping";
|
|
171
196
|
/**
|
|
172
197
|
* The reopen service worker (registered by the capture script). Root-level
|
|
173
198
|
* path so its default script-directory scope already covers `/`.
|
|
@@ -206,12 +231,49 @@ export declare class PairingEventsStream {
|
|
|
206
231
|
/** Stream count (tests/diagnostics). */
|
|
207
232
|
get size(): number;
|
|
208
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* The manual override of the automatic network choice, for the case where the
|
|
236
|
+
* phone's own probe misjudges (a firewall that answers the reachability ping
|
|
237
|
+
* but not the page, or a LAN the probe cannot see).
|
|
238
|
+
*
|
|
239
|
+
* - `auto` (default): the QR link names the public base when one is configured
|
|
240
|
+
* and the landing page probes the LAN candidates, switching to the first one
|
|
241
|
+
* that answers.
|
|
242
|
+
* - `public`: the public base only, and the landing page never probes. For a
|
|
243
|
+
* deployment where the LAN hop is undesirable.
|
|
244
|
+
* - `lan`: the first LAN literal directly, with no public hop. For a phone that
|
|
245
|
+
* is known to sit on this machine's own network.
|
|
246
|
+
*/
|
|
247
|
+
export type PairingSourcePreference = 'auto' | 'public' | 'lan';
|
|
248
|
+
/**
|
|
249
|
+
* The accepted {@link PairingSourcePreference} values, shared with the config
|
|
250
|
+
* schema so the patch that admits a value and the handler that reads it cannot
|
|
251
|
+
* drift apart.
|
|
252
|
+
*/
|
|
253
|
+
export declare const PAIRING_SOURCE_OPTIONS: readonly PairingSourcePreference[];
|
|
254
|
+
/**
|
|
255
|
+
* The LAN origins a landing page served from one authority may hop to: every
|
|
256
|
+
* LAN base the service advertises except the authority serving the request
|
|
257
|
+
* (hopping to the current origin would be a no-op), normalized through `URL`
|
|
258
|
+
* so a default port compares equal to `location.origin`.
|
|
259
|
+
* @param lanBases - the advertised LAN base URLs, in interface order.
|
|
260
|
+
* @param currentOrigin - the origin serving the landing page.
|
|
261
|
+
* @returns the distinct candidate origins, in interface order.
|
|
262
|
+
*/
|
|
263
|
+
export declare function lanSourceCandidates(lanBases: readonly string[], currentOrigin: string): string[];
|
|
209
264
|
/** Route-family dependencies (test seam). */
|
|
210
265
|
export interface PairRoutesDeps {
|
|
211
266
|
/** The pairing service. */
|
|
212
267
|
service: PairingService;
|
|
213
268
|
/** Current desktop gate policy, re-read for every status response. */
|
|
214
269
|
requirePairingForLan?: boolean | (() => boolean);
|
|
270
|
+
/**
|
|
271
|
+
* The manual override of the automatic network choice
|
|
272
|
+
* ({@link PairingSourcePreference}), re-read per request. Defaults to
|
|
273
|
+
* `auto`: the QR link names the public base and the landing page decides
|
|
274
|
+
* whether to switch to a LAN origin.
|
|
275
|
+
*/
|
|
276
|
+
pairingSource?: () => PairingSourcePreference;
|
|
215
277
|
/**
|
|
216
278
|
* LAN-bind facts for the settings card (managed patch block state, live
|
|
217
279
|
* bind host/port, firewall summary). Re-read per request so a hot-reloaded
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;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,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"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"}
|
package/lib/types/routes.js
CHANGED
|
@@ -26,6 +26,7 @@ import { isLoopbackClient, readCookie } from "./gate.js";
|
|
|
26
26
|
import { readJsonBody, writeJson } from "./http.js";
|
|
27
27
|
import { createPairGrantStore } from "./pair-grant.js";
|
|
28
28
|
import { REMOTE_HOST_GRANT_GLOBAL } from "./remote-channel-rules.js";
|
|
29
|
+
import { appShellLanSwitchScript } from "./lan-source.js";
|
|
29
30
|
/**
|
|
30
31
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
31
32
|
* package's internal fence semantics (Host/Origin based, DNS-rebinding and
|
|
@@ -299,18 +300,26 @@ const COOKIE_MAX_AGE_SEC = 365 * 24 * 60 * 60;
|
|
|
299
300
|
* The cookieless device credential: pass the device id from the /pair-app
|
|
300
301
|
* URL into sessionStorage (and localStorage for tab reloads) before any app
|
|
301
302
|
* script runs - same key the boot patch and the channel gate read. The
|
|
302
|
-
* replaceState
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
303
|
+
* replaceState drops the one-time grant from the address bar and leaves the
|
|
304
|
+
* SPA on a path this plugin answers again; which path depends on the origin.
|
|
305
|
+
*
|
|
306
|
+
* A secure context registers the reopen service worker in the same breath,
|
|
307
|
+
* and that worker owns navigations to '/', so the SPA keeps its canonical
|
|
308
|
+
* root. A plain-HTTP LAN origin is never a secure context, so the worker
|
|
309
|
+
* cannot exist there and '/' falls through to the harness index gate the
|
|
310
|
+
* cookieless flow can never satisfy (issue #1867). The address bar therefore
|
|
311
|
+
* keeps the plugin-owned landing path, which re-validates the device
|
|
312
|
+
* credential on every reload and serves the bilingual re-pair page once the
|
|
313
|
+
* pairing is gone. Both targets are bare paths: the one-time grant never
|
|
314
|
+
* survives the rewrite, and no credential is ever left in the URL.
|
|
307
315
|
*/
|
|
308
316
|
export const APP_DEVICE_STORAGE_KEY = 'dsh-remote-device';
|
|
309
317
|
export function appShellCaptureScript(deviceId) {
|
|
310
318
|
const safeId = JSON.stringify(deviceId);
|
|
311
319
|
const grantGlobal = JSON.stringify(REMOTE_HOST_GRANT_GLOBAL);
|
|
320
|
+
const reopenPath = JSON.stringify(PAIR_PATHS.appPage);
|
|
312
321
|
const register = `try{if('serviceWorker' in navigator){navigator.serviceWorker.register(${JSON.stringify(PAIR_PATHS.appServiceWorker)},{scope:'/'}).catch(function(e){})}}catch(e){}`;
|
|
313
|
-
return `<script>(function(){try{sessionStorage.setItem(${JSON.stringify(APP_DEVICE_STORAGE_KEY)},${safeId});}catch(e){}try{history.replaceState(null,'','/')}catch(e){}try{window[${grantGlobal}]=true}catch(e){}${register}})()</script>`;
|
|
322
|
+
return `<script>(function(){try{sessionStorage.setItem(${JSON.stringify(APP_DEVICE_STORAGE_KEY)},${safeId});}catch(e){}try{history.replaceState(null,'',window.isSecureContext?'/':${reopenPath})}catch(e){}try{window[${grantGlobal}]=true}catch(e){}${register}})()</script>`;
|
|
314
323
|
}
|
|
315
324
|
/**
|
|
316
325
|
* Patch the official index document with the device-capture script. The script
|
|
@@ -319,9 +328,19 @@ export function appShellCaptureScript(deviceId) {
|
|
|
319
328
|
* hook only when this server-served marker is already set (see
|
|
320
329
|
* remote-channel-boot.ts), so host mode is granted by the device-gated landing
|
|
321
330
|
* instead of being asserted from the origin.
|
|
331
|
+
*
|
|
332
|
+
* A landing reached on a base other than the machine's own LAN origin also
|
|
333
|
+
* carries the source-decision script (lan-source.ts): the phone decides at
|
|
334
|
+
* connect time whether a LAN candidate answers, and switches its origin when
|
|
335
|
+
* one does. It runs after the capture script so the granted page is fully
|
|
336
|
+
* usable before the probe's outcome can move it.
|
|
337
|
+
* @param html - the official index document.
|
|
338
|
+
* @param deviceId - the device session this landing is being served for.
|
|
339
|
+
* @param hop - the LAN candidates to probe, when a switch is possible.
|
|
340
|
+
* @returns the patched document.
|
|
322
341
|
*/
|
|
323
|
-
export function patchAppShell(html, deviceId) {
|
|
324
|
-
const script = appShellCaptureScript(deviceId);
|
|
342
|
+
export function patchAppShell(html, deviceId, hop) {
|
|
343
|
+
const script = appShellCaptureScript(deviceId) + (hop === undefined ? '' : appShellLanSwitchScript(hop));
|
|
325
344
|
const head = /<head[^>]*>/i.exec(html);
|
|
326
345
|
if (head === null)
|
|
327
346
|
return script + html;
|
|
@@ -336,9 +355,10 @@ export function patchAppShell(html, deviceId) {
|
|
|
336
355
|
* worker owns navigations to `/` instead: network-first through /pair-app
|
|
337
356
|
* (which validates the device cookie and refreshes its presence), the
|
|
338
357
|
* cached shell for offline opens, and a pass-through of the original
|
|
339
|
-
* request when the plugin no longer answers.
|
|
340
|
-
* secure
|
|
341
|
-
*
|
|
358
|
+
* request when the plugin no longer answers. A plain-HTTP LAN origin is
|
|
359
|
+
* never a secure context, so the worker cannot exist there; the capture
|
|
360
|
+
* script keeps the plugin-owned landing path in the address bar instead,
|
|
361
|
+
* and that landing re-validates the device credential on every reload.
|
|
342
362
|
*
|
|
343
363
|
* Kept as a plain string (same pattern as the capture script): it must load
|
|
344
364
|
* with no build step, run on every JS engine that ships service workers,
|
|
@@ -437,6 +457,12 @@ export const PAIR_PATHS = {
|
|
|
437
457
|
acceptPage: '/pair-accept',
|
|
438
458
|
/** The cookieless app landing: serves the official shell for a paired device. */
|
|
439
459
|
appPage: '/pair-app',
|
|
460
|
+
/**
|
|
461
|
+
* The landing page's LAN reachability beacon (lan-source.ts): an inert
|
|
462
|
+
* no-cors GET target, so a probe proves the origin answers without touching
|
|
463
|
+
* a credential or any state.
|
|
464
|
+
*/
|
|
465
|
+
ping: '/pair-ping',
|
|
440
466
|
/**
|
|
441
467
|
* The reopen service worker (registered by the capture script). Root-level
|
|
442
468
|
* path so its default script-directory scope already covers `/`.
|
|
@@ -517,6 +543,37 @@ export class PairingEventsStream {
|
|
|
517
543
|
return this.streams.size;
|
|
518
544
|
}
|
|
519
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* The accepted {@link PairingSourcePreference} values, shared with the config
|
|
548
|
+
* schema so the patch that admits a value and the handler that reads it cannot
|
|
549
|
+
* drift apart.
|
|
550
|
+
*/
|
|
551
|
+
export const PAIRING_SOURCE_OPTIONS = ['auto', 'public', 'lan'];
|
|
552
|
+
/**
|
|
553
|
+
* The LAN origins a landing page served from one authority may hop to: every
|
|
554
|
+
* LAN base the service advertises except the authority serving the request
|
|
555
|
+
* (hopping to the current origin would be a no-op), normalized through `URL`
|
|
556
|
+
* so a default port compares equal to `location.origin`.
|
|
557
|
+
* @param lanBases - the advertised LAN base URLs, in interface order.
|
|
558
|
+
* @param currentOrigin - the origin serving the landing page.
|
|
559
|
+
* @returns the distinct candidate origins, in interface order.
|
|
560
|
+
*/
|
|
561
|
+
export function lanSourceCandidates(lanBases, currentOrigin) {
|
|
562
|
+
const origins = [];
|
|
563
|
+
for (const base of lanBases) {
|
|
564
|
+
let origin;
|
|
565
|
+
try {
|
|
566
|
+
origin = new URL(base).origin;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (origin === currentOrigin || origins.includes(origin))
|
|
572
|
+
continue;
|
|
573
|
+
origins.push(origin);
|
|
574
|
+
}
|
|
575
|
+
return origins;
|
|
576
|
+
}
|
|
520
577
|
/**
|
|
521
578
|
* Build the /api/pair route family.
|
|
522
579
|
* @param deps - service + fence inputs.
|
|
@@ -527,6 +584,28 @@ export function makeRoutes(deps) {
|
|
|
527
584
|
const pairingRequired = () => typeof requirePairingForLan === 'function'
|
|
528
585
|
? requirePairingForLan()
|
|
529
586
|
: requirePairingForLan;
|
|
587
|
+
/** The manual network override, re-read per request (default: automatic). */
|
|
588
|
+
const sourcePreference = () => deps.pairingSource?.() ?? 'auto';
|
|
589
|
+
/**
|
|
590
|
+
* Every authority this surface serves, as {@link isTrustedHost} wants them
|
|
591
|
+
* (bare `host` or `host:port` literals). The LAN literals are re-read per
|
|
592
|
+
* request: a hot rebind (the lan-bind toggle) updates them mid-process, and
|
|
593
|
+
* both the fence and the reachability beacon must follow.
|
|
594
|
+
*/
|
|
595
|
+
const servedAuthorities = () => {
|
|
596
|
+
const publicHost = publicHostOf(service.publicBaseUrl);
|
|
597
|
+
const extraHosts = typeof deps.trustedHosts === 'function' ? deps.trustedHosts() : (deps.trustedHosts ?? []);
|
|
598
|
+
return [
|
|
599
|
+
...service.lanAddresses,
|
|
600
|
+
...(publicHost !== undefined ? [publicHost] : []),
|
|
601
|
+
...extraHosts,
|
|
602
|
+
...dynamicTrustedHosts,
|
|
603
|
+
];
|
|
604
|
+
};
|
|
605
|
+
/** Every LAN base URL the service currently advertises, in interface order. */
|
|
606
|
+
const lanBaseUrls = () => service.lanAddresses
|
|
607
|
+
.map(address => service.lanBaseUrlFor(address))
|
|
608
|
+
.filter((base) => base !== undefined);
|
|
530
609
|
const grants = deps.grants ?? createPairGrantStore();
|
|
531
610
|
const events = new PairingEventsStream(service);
|
|
532
611
|
const dynamicTrustedHosts = new Set();
|
|
@@ -539,17 +618,7 @@ export function makeRoutes(deps) {
|
|
|
539
618
|
* pairing entry pages), never for an API or subresource request.
|
|
540
619
|
*/
|
|
541
620
|
const lanFence = (req, navigation = false) => {
|
|
542
|
-
const
|
|
543
|
-
// The service's LAN bases re-read per request: a hot rebind (the lan-bind
|
|
544
|
-
// toggle) updates them mid-process, and the fence must follow.
|
|
545
|
-
const bases = service.lanAddresses;
|
|
546
|
-
const extraHosts = typeof deps.trustedHosts === 'function' ? deps.trustedHosts() : (deps.trustedHosts ?? []);
|
|
547
|
-
const combined = [
|
|
548
|
-
...bases,
|
|
549
|
-
...(publicHost !== undefined ? [publicHost] : []),
|
|
550
|
-
...extraHosts,
|
|
551
|
-
...dynamicTrustedHosts,
|
|
552
|
-
];
|
|
621
|
+
const combined = servedAuthorities();
|
|
553
622
|
if (navigation && isTopLevelDocumentNavigation(req) ? isTrustedHost(req, combined) : isTrustedApiRequest(req, combined))
|
|
554
623
|
return true;
|
|
555
624
|
// If request carries a valid paired device cookie from a private LAN host (e.g. after service restart), trust and remember it
|
|
@@ -662,10 +731,23 @@ export function makeRoutes(deps) {
|
|
|
662
731
|
// the injected portrait adaptation, PCs the full desktop UI), so the
|
|
663
732
|
// remote surface can never drift from the official one.
|
|
664
733
|
const { token, expiresAt } = service.issue(undefined, address);
|
|
665
|
-
// The
|
|
666
|
-
// phone anywhere can reach
|
|
667
|
-
//
|
|
668
|
-
|
|
734
|
+
// The advertised base is the public (tunneled/relayed) one whenever it is
|
|
735
|
+
// configured: it is the only base a phone anywhere can reach, so the QR
|
|
736
|
+
// never depends on the scanning phone being on this machine's network.
|
|
737
|
+
// The landing page then decides at connect time whether to move to a LAN
|
|
738
|
+
// origin (lan-source.ts). `lan` is the manual override that pins the QR to
|
|
739
|
+
// a LAN literal; an explicit address always names one.
|
|
740
|
+
const preference = sourcePreference();
|
|
741
|
+
const base = address !== undefined
|
|
742
|
+
? service.lanBaseUrlFor(address)
|
|
743
|
+
: preference === 'lan'
|
|
744
|
+
? service.lanBaseUrl
|
|
745
|
+
: preference === 'public'
|
|
746
|
+
// Pinned to public: with none configured there is no satisfiable
|
|
747
|
+
// link, which the caller reports as lan-required rather than
|
|
748
|
+
// silently advertising a base the override excluded.
|
|
749
|
+
? service.publicBaseUrl
|
|
750
|
+
: (service.publicBaseUrl ?? service.lanBaseUrl);
|
|
669
751
|
if (base === undefined)
|
|
670
752
|
throw new Error('remote-web-ui: base unavailable');
|
|
671
753
|
writeJson(res, 200, {
|
|
@@ -954,7 +1036,51 @@ export function makeRoutes(deps) {
|
|
|
954
1036
|
'referrer-policy': 'no-referrer',
|
|
955
1037
|
'set-cookie': [deviceCookie(req, service.config.cookieName, id)],
|
|
956
1038
|
});
|
|
957
|
-
res.end(patchAppShell(html, id));
|
|
1039
|
+
res.end(patchAppShell(html, id, lanHop(req, id)));
|
|
1040
|
+
};
|
|
1041
|
+
/**
|
|
1042
|
+
* The LAN hop this landing may take, or undefined when there is nothing to
|
|
1043
|
+
* decide. Three conditions have to hold together: the automatic preference
|
|
1044
|
+
* (both overrides answer the question already — `lan` serves the QR from a
|
|
1045
|
+
* LAN origin, `public` forbids the move), at least one LAN candidate other
|
|
1046
|
+
* than this origin, and a fresh one-time grant for the target landing. The
|
|
1047
|
+
* grant is what lets the target origin serve the shell: the device cookie is
|
|
1048
|
+
* origin-scoped, so the LAN origin has no credential of its own, and the
|
|
1049
|
+
* grant table lives in this process regardless of which interface answered.
|
|
1050
|
+
* The candidate set is carried to the client as origins (lan-source.ts
|
|
1051
|
+
* compares them against `location.origin`).
|
|
1052
|
+
* @param req - the landing request (its authority is the current origin).
|
|
1053
|
+
* @param deviceId - the device session the landing is served for.
|
|
1054
|
+
* @returns the hop payload, or undefined when no switch is possible.
|
|
1055
|
+
*/
|
|
1056
|
+
const lanHop = (req, deviceId) => {
|
|
1057
|
+
if (sourcePreference() !== 'auto')
|
|
1058
|
+
return undefined;
|
|
1059
|
+
const candidates = lanSourceCandidates(lanBaseUrls(), appOrigin(req));
|
|
1060
|
+
if (candidates.length === 0)
|
|
1061
|
+
return undefined;
|
|
1062
|
+
return { candidates, path: PAIR_PATHS.appPage, pingPath: PAIR_PATHS.ping, grant: grants.issue(deviceId).grant };
|
|
1063
|
+
};
|
|
1064
|
+
/**
|
|
1065
|
+
* The landing page's LAN reachability beacon: a 204 for a Host this plugin
|
|
1066
|
+
* serves, a 403 otherwise. It carries no credential and no state — a phone
|
|
1067
|
+
* that reaches it has proved the origin answers, which is all the source
|
|
1068
|
+
* decision needs, and the fence's browser-marker half is deliberately not
|
|
1069
|
+
* applied here: the probe is cross-site by construction (a page on the
|
|
1070
|
+
* public origin fetching a LAN origin), so the marker check would turn every
|
|
1071
|
+
* probe into the same 403 that means "not ours" and make the two
|
|
1072
|
+
* indistinguishable.
|
|
1073
|
+
*/
|
|
1074
|
+
const handlePing = (req, res) => {
|
|
1075
|
+
if (!requireMethod(req, res, 'GET'))
|
|
1076
|
+
return;
|
|
1077
|
+
if (!isTrustedHost(req, servedAuthorities())) {
|
|
1078
|
+
res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
1079
|
+
res.end();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
res.writeHead(204, { 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' });
|
|
1083
|
+
res.end();
|
|
958
1084
|
};
|
|
959
1085
|
/**
|
|
960
1086
|
* The reopen service worker script. Same phone-facing fence as the app
|
|
@@ -993,6 +1119,10 @@ export function makeRoutes(deps) {
|
|
|
993
1119
|
routes.push({ kind: 'exact', path: PAIR_PATHS.lanBind, handler: handleLanBind });
|
|
994
1120
|
}
|
|
995
1121
|
routes.push({ kind: 'exact', path: PAIR_PATHS.acceptPage, handler: handleAcceptPage });
|
|
1122
|
+
// The beacon is independent of the app landing: a deployment with no
|
|
1123
|
+
// indexDocument still advertises LAN bases, and a landing served some other
|
|
1124
|
+
// way may still probe them.
|
|
1125
|
+
routes.push({ kind: 'exact', path: PAIR_PATHS.ping, handler: handlePing });
|
|
996
1126
|
if (deps.indexDocument !== undefined) {
|
|
997
1127
|
routes.push({ kind: 'exact', path: PAIR_PATHS.appPage, handler: handleAppPage });
|
|
998
1128
|
// The reopen worker is only meaningful when the app landing it
|
|
@@ -25,12 +25,18 @@ export type TunnelPlan = {
|
|
|
25
25
|
* token AND a valid public hostname (the token does not carry the hostname,
|
|
26
26
|
* so the QR base and the fence trust would have no source without it);
|
|
27
27
|
* otherwise no tunnel and the manual public base applies.
|
|
28
|
+
*
|
|
29
|
+
* The quick target URL names the local webServer port, so a plan made before
|
|
30
|
+
* the listener binds cannot be quick: it comes back `off` and the caller —
|
|
31
|
+
* which can observe the port settle — re-plans. The planner itself never
|
|
32
|
+
* emits an `http://127.0.0.1:undefined` target.
|
|
28
33
|
* @param value - the resolved settings slice that drives the tunnel.
|
|
29
|
-
* @param port - the local webServer port the tunnel forwards to
|
|
34
|
+
* @param port - the local webServer port the tunnel forwards to (undefined
|
|
35
|
+
* before the listener binds).
|
|
30
36
|
*/
|
|
31
37
|
export declare function tunnelPlanOf(value: {
|
|
32
38
|
autoTunnel?: boolean;
|
|
33
39
|
tunnelToken?: string;
|
|
34
40
|
publicBaseUrl?: string;
|
|
35
|
-
}, port: number): TunnelPlan;
|
|
41
|
+
}, port: number | undefined): TunnelPlan;
|
|
36
42
|
//# sourceMappingURL=tunnel-plan.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-plan.d.ts","sourceRoot":"","sources":["../../src/tunnel-plan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOhD;AAED,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,aAAa,GAAG,eAAe,CAAC,CAAA;CAAE,GACrF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD
|
|
1
|
+
{"version":3,"file":"tunnel-plan.d.ts","sourceRoot":"","sources":["../../src/tunnel-plan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOhD;AAED,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,aAAa,GAAG,eAAe,CAAC,CAAA;CAAE,GACrF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7E,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,UAAU,CAeZ"}
|
package/lib/types/tunnel-plan.js
CHANGED
|
@@ -21,11 +21,19 @@ export function isHttpUrl(value) {
|
|
|
21
21
|
* token AND a valid public hostname (the token does not carry the hostname,
|
|
22
22
|
* so the QR base and the fence trust would have no source without it);
|
|
23
23
|
* otherwise no tunnel and the manual public base applies.
|
|
24
|
+
*
|
|
25
|
+
* The quick target URL names the local webServer port, so a plan made before
|
|
26
|
+
* the listener binds cannot be quick: it comes back `off` and the caller —
|
|
27
|
+
* which can observe the port settle — re-plans. The planner itself never
|
|
28
|
+
* emits an `http://127.0.0.1:undefined` target.
|
|
24
29
|
* @param value - the resolved settings slice that drives the tunnel.
|
|
25
|
-
* @param port - the local webServer port the tunnel forwards to
|
|
30
|
+
* @param port - the local webServer port the tunnel forwards to (undefined
|
|
31
|
+
* before the listener binds).
|
|
26
32
|
*/
|
|
27
33
|
export function tunnelPlanOf(value, port) {
|
|
28
34
|
if (value.autoTunnel === true) {
|
|
35
|
+
if (port === undefined)
|
|
36
|
+
return { mode: 'off' };
|
|
29
37
|
const ignored = [];
|
|
30
38
|
if (value.tunnelToken !== undefined && value.tunnelToken !== '')
|
|
31
39
|
ignored.push('tunnelToken');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and revocable device sessions, with a LAN bind toggle, optional Cloudflare tunnel, and one-click family self-update",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.7",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
"types": "./lib/types/index.d.ts",
|
|
15
15
|
"default": "./lib/index.js"
|
|
16
16
|
},
|
|
17
|
-
"./invariant": {
|
|
18
|
-
"types": "./lib/types/invariant.d.ts",
|
|
19
|
-
"default": "./lib/invariant.js"
|
|
20
|
-
},
|
|
21
17
|
"./client": {
|
|
22
18
|
"types": "./lib/types/client/index.d.ts",
|
|
23
19
|
"default": "./lib/client.js"
|
|
@@ -38,7 +34,7 @@
|
|
|
38
34
|
"license": "Apache-2.0",
|
|
39
35
|
"dsh": {
|
|
40
36
|
"engines": {
|
|
41
|
-
"dsh": ">=0.2.
|
|
37
|
+
"dsh": ">=0.2.1-rc.2"
|
|
42
38
|
},
|
|
43
39
|
"bundle": {
|
|
44
40
|
"patch": "./cordis.patch.yml"
|
|
@@ -53,7 +49,7 @@
|
|
|
53
49
|
}
|
|
54
50
|
},
|
|
55
51
|
"dependencies": {
|
|
56
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
52
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
57
53
|
"cloudflared": "^0.7.3",
|
|
58
54
|
"clsx": "^2.1.1",
|
|
59
55
|
"qrcode.react": "^4.2.0",
|
|
@@ -61,28 +57,27 @@
|
|
|
61
57
|
"zod": "^4.4.3"
|
|
62
58
|
},
|
|
63
59
|
"peerDependencies": {
|
|
64
|
-
"@deepseek-ai/dsh": ">=0.2.
|
|
60
|
+
"@deepseek-ai/dsh": ">=0.2.1-rc.2",
|
|
65
61
|
"react": "^18.2.0",
|
|
66
62
|
"react-dom": "^18.2.0"
|
|
67
63
|
},
|
|
68
64
|
"devDependencies": {
|
|
69
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
70
|
-
"@deepseek-ai/dsh-agent": "^0.2.
|
|
71
|
-
"@deepseek-ai/dsh-api-remotes": "^0.2.
|
|
72
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.2.
|
|
73
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.
|
|
74
|
-
"@deepseek-ai/dsh-client-connection": "^0.2.
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
76
|
-
"@deepseek-ai/dsh-client-store": "^0.2.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.
|
|
81
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.
|
|
82
|
-
"@deepseek-ai/dsh-commands": "^0.2.
|
|
83
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.
|
|
84
|
-
"@deepseek-ai/dsh-
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
66
|
+
"@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-client-store": "^0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.1-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-commands": "^0.2.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
|
|
86
81
|
"@testing-library/dom": "^10.4.1",
|
|
87
82
|
"@testing-library/react": "^16.3.2",
|
|
88
83
|
"@types/node": "^22.20.0",
|
|
@@ -73,10 +73,10 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
73
73
|
eventSource.current = undefined
|
|
74
74
|
}, [])
|
|
75
75
|
|
|
76
|
-
const mint = useCallback(async (
|
|
76
|
+
const mint = useCallback(async (): Promise<PanelState> => {
|
|
77
77
|
let result: IssueResponse
|
|
78
78
|
try {
|
|
79
|
-
result = await issuePair(
|
|
79
|
+
result = await issuePair()
|
|
80
80
|
} catch {
|
|
81
81
|
// Fetch/network failure: show an explicit state instead of silently
|
|
82
82
|
// leaving the panel on its initial banner.
|
|
@@ -104,10 +104,6 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
104
104
|
// Whether this QR is built on the configured public (tunneled) base.
|
|
105
105
|
public: publicBaseUrl !== undefined && result.url.startsWith(publicBaseUrl),
|
|
106
106
|
...(publicBaseUrl !== undefined ? { publicBaseUrl } : {}),
|
|
107
|
-
// The issued URL names the requested (or default first) literal; the
|
|
108
|
-
// public link has no LAN literal, so no radio row is selected then.
|
|
109
|
-
address: address ?? result.lanAddresses[0] ?? '',
|
|
110
|
-
lanAddresses: result.lanAddresses,
|
|
111
107
|
}
|
|
112
108
|
}, [])
|
|
113
109
|
|
|
@@ -203,16 +199,6 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
203
199
|
void mint().then(setState)
|
|
204
200
|
}, [mint])
|
|
205
201
|
|
|
206
|
-
/** Re-mint against another LAN literal (multi-homed machines). */
|
|
207
|
-
const handlePickAddress = useCallback((address: string) => {
|
|
208
|
-
void mint(address).then(setState)
|
|
209
|
-
}, [mint])
|
|
210
|
-
|
|
211
|
-
/** Re-mint against the configured public (tunneled) base. */
|
|
212
|
-
const handlePickPublic = useCallback(() => {
|
|
213
|
-
void mint().then(setState)
|
|
214
|
-
}, [mint])
|
|
215
|
-
|
|
216
202
|
const handleCopy = useCallback((url: string) => {
|
|
217
203
|
void copyText(url).then((ok) => {
|
|
218
204
|
if (!ok) return
|
|
@@ -247,8 +233,6 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
247
233
|
onRefresh={handleRefresh}
|
|
248
234
|
onCopy={handleCopy}
|
|
249
235
|
onCopyToken={handleCopyToken}
|
|
250
|
-
onPickAddress={handlePickAddress}
|
|
251
|
-
onPickPublic={handlePickPublic}
|
|
252
236
|
onRevoke={handleRevoke}
|
|
253
237
|
/>
|
|
254
238
|
</div>
|