@linxin666/dsh-remote-web-ui 0.4.1 → 0.4.2

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.
Files changed (46) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +132 -29
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +117 -15
  7. package/lib/types/client/RemoteEntry.js +1 -1
  8. package/lib/types/client/index.d.ts.map +1 -1
  9. package/lib/types/client/index.js +5 -3
  10. package/lib/types/client/locales.js +2 -2
  11. package/lib/types/client/pair-api.d.ts +4 -0
  12. package/lib/types/client/pair-api.d.ts.map +1 -1
  13. package/lib/types/client/pair-api.js +11 -7
  14. package/lib/types/client/remote-channel.d.ts +27 -7
  15. package/lib/types/client/remote-channel.d.ts.map +1 -1
  16. package/lib/types/client/remote-channel.js +25 -17
  17. package/lib/types/client/update-api.js +2 -2
  18. package/lib/types/lan-bind.d.ts +4 -0
  19. package/lib/types/lan-bind.d.ts.map +1 -1
  20. package/lib/types/lan-bind.js +64 -1
  21. package/lib/types/pairing.d.ts +15 -2
  22. package/lib/types/pairing.d.ts.map +1 -1
  23. package/lib/types/pairing.js +26 -14
  24. package/lib/types/pairing.persist.test.js +43 -3
  25. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  26. package/lib/types/remote-channel-boot.js +13 -3
  27. package/lib/types/remote-channel-rules.d.ts +74 -0
  28. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  29. package/lib/types/remote-channel-rules.js +104 -0
  30. package/lib/types/update.d.ts +10 -0
  31. package/lib/types/update.d.ts.map +1 -1
  32. package/lib/types/update.js +32 -3
  33. package/package.json +20 -19
  34. package/src/client/RemoteEntry.tsx +1 -1
  35. package/src/client/index.ts +5 -3
  36. package/src/client/locales.ts +2 -2
  37. package/src/client/pair-api.ts +11 -7
  38. package/src/client/remote-channel.ts +38 -15
  39. package/src/client/remote.module.css +18 -0
  40. package/src/client/update-api.ts +2 -2
  41. package/src/lan-bind.ts +62 -1
  42. package/src/pairing.persist.test.ts +47 -3
  43. package/src/pairing.ts +26 -12
  44. package/src/remote-channel-boot.ts +13 -3
  45. package/src/remote-channel-rules.ts +113 -0
  46. package/src/update.ts +36 -3
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
22
22
  import { dirname, join, resolve, sep } from 'node:path';
23
+ import { parseDocument } from 'yaml';
23
24
  import { dshHome } from "./dsh-home.js";
24
25
  /** Profile names safe to interpolate into the patch path: one path segment, no traversal. */
25
26
  const PROFILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
@@ -59,6 +60,64 @@ export function stripManagedBlock(content) {
59
60
  const pattern = new RegExp(`\\r?\\n?${begin}[\\s\\S]*?${end}\\r?\\n?`, 'g');
60
61
  return content.replace(pattern, '\n');
61
62
  }
63
+ /**
64
+ * Render the managed block for one bind state. The values are static: the
65
+ * user patch layer has no reliable lazy service evaluation, and the plugin
66
+ * re-asserts the block at every boot so CLI flags (--port, --host) win by
67
+ * rewriting it before the next start.
68
+ */
69
+ /**
70
+ * Whether the base opens a flow collection at the root. That is the shape the
71
+ * plugin manager's YAML document round-trip leaves behind when it appends rows
72
+ * to the profile's placeholder `[]`: `[ { id: … }, … ]`. Every other base
73
+ * (empty, block sequence, comment-only prefix) keeps the historical string
74
+ * concatenation, so the common paths stay byte-identical.
75
+ */
76
+ function isFlowRoot(base) {
77
+ return base.trimStart().startsWith('[');
78
+ }
79
+ /** Force one parsed collection (and its item collections) into block style. */
80
+ function toBlockStyle(node) {
81
+ if (typeof node !== 'object' || node === null)
82
+ return;
83
+ const collection = node;
84
+ if (!Array.isArray(collection.items) && typeof collection.flow !== 'boolean')
85
+ return;
86
+ collection.flow = false;
87
+ for (const item of collection.items ?? [])
88
+ toBlockStyle(item);
89
+ }
90
+ /** Plain-value view of one parsed node (YAML nodes keep their own accessors). */
91
+ function plainOf(node) {
92
+ if (typeof node !== 'object' || node === null)
93
+ return node;
94
+ const json = node.toJSON;
95
+ return typeof json === 'function' ? json.call(node) : node;
96
+ }
97
+ /**
98
+ * Produce the file text for one bind state. A flow-style base is re-emitted as
99
+ * a block sequence before the managed block is appended, so the file stays
100
+ * exactly one valid YAML document; concatenating the block onto a non-empty
101
+ * flow array produced two root documents and the profile failed to parse at
102
+ * `dsh web` startup (#1675). A base that cannot be parsed is refused with the
103
+ * file path rather than written half-valid.
104
+ */
105
+ function mergePatchContent(base, block, file) {
106
+ if (base.length === 0)
107
+ return block;
108
+ if (!isFlowRoot(base))
109
+ return `${base}\n\n${block}`;
110
+ const doc = parseDocument(base, { uniqueKeys: false });
111
+ if (doc.errors.length > 0 || doc.contents === null) {
112
+ throw new Error(`remote-web-ui: cannot update the lan-bind block in ${file}: ${doc.errors[0]?.message ?? 'the patch file is not a YAML document'}`);
113
+ }
114
+ if (!('items' in doc.contents)) {
115
+ throw new Error(`remote-web-ui: cannot update the lan-bind block in ${file}: the patch file is not a top-level patch list`);
116
+ }
117
+ toBlockStyle(doc.contents);
118
+ const rendered = doc.toString({ lineWidth: 0 }).trimEnd();
119
+ return `${rendered}\n\n${block}`;
120
+ }
62
121
  /**
63
122
  * Render the managed block for one bind state. The values are static: the
64
123
  * user patch layer has no reliable lazy service evaluation, and the plugin
@@ -113,6 +172,10 @@ export function lanBindState(profile, home = dshHome()) {
113
172
  * unterminated block (BEGIN marker without END, which stripManagedBlock
114
173
  * cannot match) is truncated at its BEGIN marker first so the rewrite can
115
174
  * never stack a second webserver row onto the orphan.
175
+ *
176
+ * The written file is always exactly one valid YAML document: a base the
177
+ * plugin manager left in flow style is re-emitted as a block sequence before
178
+ * the managed block is appended (see mergePatchContent).
116
179
  */
117
180
  export function writeLanBind(host, port, profile, home = dshHome()) {
118
181
  const file = profilePatchFile(profile, home);
@@ -121,7 +184,7 @@ export function writeLanBind(host, port, profile, home = dshHome()) {
121
184
  const rawBase = (orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd();
122
185
  const base = rawBase.replace(/\[\s*\]\s*$/, '').trimEnd();
123
186
  const block = managedBlock(host, port);
124
- const content = base.length > 0 ? `${base}\n\n${block}` : block;
187
+ const content = mergePatchContent(base, block, file);
125
188
  const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
126
189
  mkdirSync(dirname(file), { recursive: true });
127
190
  const temp = `${file}.remote-web-ui-tmp-${process.pid.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -219,10 +219,16 @@ export declare class PairingService {
219
219
  * Restore device sessions persisted by a previous process run. A corrupt
220
220
  * or missing file is tolerated (an empty device table, never a throw) —
221
221
  * persistence is an availability convenience, not a security boundary.
222
+ *
223
+ * Every still-valid session is restored, deliberately without applying
224
+ * `maxDevices`: the constructor runs with whatever cap the configuration
225
+ * layer has delivered so far (the schema default until the saved settings
226
+ * row is applied), and trimming here would permanently drop authorizations
227
+ * the user never revoked — the trimmed table reaches disk on the next
228
+ * heartbeat/sweep write. The cap is enforced where a device is admitted
229
+ * (see `accept`), against the configuration in force at that moment.
222
230
  */
223
231
  private loadPersisted;
224
- /** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
225
- private clampToMaxDevices;
226
232
  /** Drop sessions whose lastSeenAt is older than idleExpireMs. */
227
233
  private evictIdle;
228
234
  /**
@@ -339,6 +345,13 @@ export declare class PairingService {
339
345
  snapshot(): PairingSnapshot;
340
346
  /** Whether a cookie value names a currently live (non-idle) device session. */
341
347
  hasDevice(deviceId: string): boolean;
348
+ /**
349
+ * Number of device sessions in the table, including idle ones whose idle
350
+ * window has not been swept yet. Restore keeps every persisted session
351
+ * (see `loadPersisted`), so this can exceed the configured cap until the
352
+ * next `accept` trims the excess.
353
+ */
354
+ deviceCount(): number;
342
355
  /** Subscribe to snapshot changes (each emit passes a fresh snapshot). */
343
356
  onState(listener: (snapshot: PairingSnapshot) => void): () => void;
344
357
  private activeToken;
@@ -1 +1 @@
1
- {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
@@ -95,6 +95,14 @@ export class PairingService {
95
95
  * Restore device sessions persisted by a previous process run. A corrupt
96
96
  * or missing file is tolerated (an empty device table, never a throw) —
97
97
  * persistence is an availability convenience, not a security boundary.
98
+ *
99
+ * Every still-valid session is restored, deliberately without applying
100
+ * `maxDevices`: the constructor runs with whatever cap the configuration
101
+ * layer has delivered so far (the schema default until the saved settings
102
+ * row is applied), and trimming here would permanently drop authorizations
103
+ * the user never revoked — the trimmed table reaches disk on the next
104
+ * heartbeat/sweep write. The cap is enforced where a device is admitted
105
+ * (see `accept`), against the configuration in force at that moment.
98
106
  */
99
107
  loadPersisted() {
100
108
  const file = this.config.devicesFile;
@@ -119,7 +127,6 @@ export class PairingService {
119
127
  ...(label !== undefined ? { userAgent: label } : {}),
120
128
  });
121
129
  }
122
- this.clampToMaxDevices();
123
130
  if (this.evictIdle())
124
131
  this.persistRevocation();
125
132
  }
@@ -127,15 +134,6 @@ export class PairingService {
127
134
  // Unreadable/corrupt: start empty rather than refusing to boot.
128
135
  }
129
136
  }
130
- /** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
131
- clampToMaxDevices() {
132
- if (this.devices.size <= this.config.maxDevices)
133
- return;
134
- const overflow = this.devices.size - this.config.maxDevices;
135
- const ordered = [...this.devices.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
136
- for (const [id] of ordered.slice(0, overflow))
137
- this.devices.delete(id);
138
- }
139
137
  /** Drop sessions whose lastSeenAt is older than idleExpireMs. */
140
138
  evictIdle() {
141
139
  const now = this.clock.now();
@@ -302,15 +300,20 @@ export class PairingService {
302
300
  }
303
301
  const deviceId = this.clock.randomToken();
304
302
  const now = this.clock.now();
305
- if (this.devices.size >= this.config.maxDevices) {
306
- // Evict the oldest session (FIFO) before binding a new device.
303
+ // FIFO-evict until a slot is free. The loop (not a single delete) covers a
304
+ // table that is already over the cap: restore keeps every persisted session
305
+ // (see loadPersisted), so the excess is collected here, once the store is
306
+ // actually asked to admit a new device, rather than at startup on a cap the
307
+ // configuration may not have delivered yet.
308
+ while (this.devices.size >= this.config.maxDevices) {
307
309
  let oldest;
308
310
  for (const [id, session] of this.devices) {
309
311
  if (oldest === undefined || session.createdAt < oldest.createdAt)
310
312
  oldest = { id, createdAt: session.createdAt };
311
313
  }
312
- if (oldest !== undefined)
313
- this.devices.delete(oldest.id);
314
+ if (oldest === undefined)
315
+ break;
316
+ this.devices.delete(oldest.id);
314
317
  }
315
318
  const label = sanitizeUserAgent(userAgent);
316
319
  this.devices.set(deviceId, {
@@ -414,6 +417,15 @@ export class PairingService {
414
417
  hasDevice(deviceId) {
415
418
  return this.liveSession(deviceId) !== undefined;
416
419
  }
420
+ /**
421
+ * Number of device sessions in the table, including idle ones whose idle
422
+ * window has not been swept yet. Restore keeps every persisted session
423
+ * (see `loadPersisted`), so this can exceed the configured cap until the
424
+ * next `accept` trims the excess.
425
+ */
426
+ deviceCount() {
427
+ return this.devices.size;
428
+ }
417
429
  /** Subscribe to snapshot changes (each emit passes a fresh snapshot). */
418
430
  onState(listener) {
419
431
  this.listeners.add(listener);
@@ -86,17 +86,57 @@ describe('PairingService device persistence', () => {
86
86
  const restarted = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
87
87
  expect(restarted.hasDevice(deviceId)).toBe(true);
88
88
  });
89
- it('clamps a persisted table to maxDevices when the file holds more sessions', () => {
89
+ // #1696: the constructor runs before the saved settings row is applied, so
90
+ // its cap can still be the schema default. Restoring must not enforce it —
91
+ // trimming there silently revokes authorizations the user never revoked,
92
+ // and the trimmed table reaches disk on the next heartbeat/sweep write.
93
+ it('operator: every valid persisted session survives a restart under the startup cap', () => {
90
94
  const file = join(dir, 'devices.json');
95
+ // Given a store holding three devices while the startup cap reads 4
96
+ // (the schema default) and the saved settings value is 12.
91
97
  writeFileSync(file, JSON.stringify({
92
98
  dev1: { createdAt: 1_000, lastSeenAt: 1_000 },
93
99
  dev2: { createdAt: 2_000, lastSeenAt: 2_000 },
94
100
  dev3: { createdAt: 3_000, lastSeenAt: 3_000 },
95
101
  }));
96
- const service = new PairingService({ ...BASE_CONFIG, maxDevices: 2, devicesFile: file }, makeClock());
97
- expect(service.hasDevice('dev3')).toBe(true);
102
+ // When a new process restores the store.
103
+ const service = new PairingService({ ...BASE_CONFIG, maxDevices: 4, devicesFile: file }, makeClock());
104
+ // Then no paired device lost its authorization to the transient cap.
105
+ expect(service.hasDevice('dev1')).toBe(true);
98
106
  expect(service.hasDevice('dev2')).toBe(true);
107
+ expect(service.hasDevice('dev3')).toBe(true);
108
+ });
109
+ it('operator: a restored over-cap table is not trimmed by a heartbeat and sweep', () => {
110
+ const file = join(dir, 'devices.json');
111
+ // Given twelve persisted devices restored under the default cap of 4.
112
+ const rows = Object.fromEntries(Array.from({ length: 12 }, (_, i) => [`dev${i}`, { createdAt: 1_000 + i, lastSeenAt: 1_000 + i }]));
113
+ writeFileSync(file, JSON.stringify(rows));
114
+ const service = new PairingService({ ...BASE_CONFIG, maxDevices: 4, devicesFile: file }, makeClock());
115
+ // When the saved cap of 12 arrives and normal traffic writes the store.
116
+ service.config = { ...service.config, maxDevices: 12 };
117
+ service.heartbeat('dev0');
118
+ service.sweep();
119
+ // Then the file still names all twelve devices.
120
+ const persisted = JSON.parse(readFileSync(file, 'utf8'));
121
+ expect(Object.keys(persisted)).toHaveLength(12);
122
+ expect(service.deviceCount()).toBe(12);
123
+ });
124
+ it('operator: the cap is enforced when a device is admitted, not before', () => {
125
+ const file = join(dir, 'devices.json');
126
+ // Given three persisted devices restored under a cap of 2.
127
+ writeFileSync(file, JSON.stringify({
128
+ dev1: { createdAt: 1_000, lastSeenAt: 1_000 },
129
+ dev2: { createdAt: 2_000, lastSeenAt: 2_000 },
130
+ dev3: { createdAt: 3_000, lastSeenAt: 3_000 },
131
+ }));
132
+ const service = new PairingService({ ...BASE_CONFIG, maxDevices: 2, devicesFile: file }, makeClock());
133
+ expect(service.deviceCount()).toBe(3);
134
+ // When one more device pairs.
135
+ const paired = pairDevice(service);
136
+ // Then the table is trimmed to the cap, oldest first, keeping the newcomer.
137
+ expect(service.deviceCount()).toBe(2);
99
138
  expect(service.hasDevice('dev1')).toBe(false);
139
+ expect(service.hasDevice(paired)).toBe(true);
100
140
  });
101
141
  it('keeps sessions memory-only when devicesFile is unset', () => {
102
142
  const first = new PairingService({ ...BASE_CONFIG }, makeClock());
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAwJrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
1
+ {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAkKrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
@@ -43,10 +43,20 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
43
43
  return '(function(){' +
44
44
  'try{' +
45
45
  'var w=window,loc=w.location,h=loc.hostname;' +
46
- // Loopback origins (including the bracketed IPv6 literal WHATWG returns)
47
- // keep the original paths (mirrors isLoopbackHostname).
48
- "if(h==='localhost'||h==='::1'||h==='[::1]'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
49
46
  'var R=' + json + ';' +
47
+ // A page the machine itself serves keeps the original paths and no
48
+ // channel: loopback names (including the bracketed IPv6 literal WHATWG
49
+ // returns), any scheme no web transport carries (an application on this
50
+ // machine delivered it - the desktop shell's own scheme, a file page, a
51
+ // scheme a newer shell introduces), or a shell the official transport
52
+ // already declared the host owner. This mirrors isLocalPage in
53
+ // remote-channel-rules.ts, which owns the decision.
54
+ "if(h==='localhost'||h==='::1'||h==='[::1]'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
55
+ 'var pr=loc.protocol;' +
56
+ 'if(typeof pr==="string"&&pr!==""&&R.webProtocols.indexOf(pr)===-1)return;' +
57
+ // ownsHost alone does not lift the fence: the plugin's own device-gated
58
+ // landing grants it to a paired LAN/tunnel page, which must stay gated.
59
+ 'try{if(w.__DSH_TRANSPORT__&&w.__DSH_TRANSPORT__.ownsHost===true&&h.indexOf(".")===-1&&h.indexOf(":")===-1)return}catch(e){}' +
50
60
  // Host mode is server-granted: only the device-gated app landing
51
61
  // publishes the grant marker (in a capture script that runs ahead of
52
62
  // this one), so a shell that merely sits on a non-loopback origin - an
@@ -8,6 +8,73 @@
8
8
  /** The gated mirror prefix (must match src/remote-methods.ts). */
9
9
  export declare const REMOTE_PREFIX = "/remote";
10
10
  import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from './remote-methods.ts';
11
+ /**
12
+ * The schemes that can deliver a page a remote party controls: the network
13
+ * transports, plus the documents a network page can mint. Every other scheme
14
+ * is registered and served by an application on this machine, so its page is
15
+ * the machine's own page whatever the application calls the scheme.
16
+ *
17
+ * The list is deliberately the *web* side rather than an allowlist of known
18
+ * desktop shells. The official DSH Desktop shell serves its Web GUI from
19
+ * `dsh-app://app/` (`location.hostname === 'app'`), which no hostname
20
+ * predicate can recognise; a scheme allowlist fixed that instance (#1682) but
21
+ * left the next shell — or a `file:` page — fenced behind a pairing page it
22
+ * can never complete, because a pairing link is reachable only over the
23
+ * network. Naming the web side instead makes an unknown application scheme
24
+ * local by construction.
25
+ *
26
+ * `blob:`, `data:`, `about:` and `filesystem:` stay on the web side: a
27
+ * network page mints those documents, so they must keep the fence.
28
+ */
29
+ export declare const WEB_PAGE_PROTOCOLS: readonly string[];
30
+ /**
31
+ * Whether one page scheme can carry a document a remote party controls.
32
+ * @param protocol - `location.protocol` of the page (for example `https:`).
33
+ * @returns true for the network transports and the documents they mint.
34
+ */
35
+ export declare function isWebPageProtocol(protocol: string): boolean;
36
+ /**
37
+ * Hostname-only loopback classification: localhost, the IPv6 loopback literal
38
+ * (WHATWG keeps its brackets) and any 127/8 IPv4 literal. Kept string-in,
39
+ * boolean-out and dependency-free so the browser half and the inlined boot
40
+ * script can both run it verbatim.
41
+ * @param hostname - a page/cookie hostname, IPv6 literals bracketed.
42
+ * @returns true for a loopback name or literal.
43
+ */
44
+ export declare function isLoopbackHostname(hostname: string): boolean;
45
+ /**
46
+ * Whether the current page is served from the machine running the host, so
47
+ * its same-origin traffic must NOT be fenced behind the pairing channel.
48
+ * Three independent facts all describe the page itself and any one is enough:
49
+ *
50
+ * - the page's own hostname is loopback;
51
+ * - the page's protocol is not a {@link WEB_PAGE_PROTOCOLS} web scheme, so an
52
+ * application on this machine delivered it — the desktop shell's
53
+ * `dsh-app://app/` is one such page, and a scheme this build has never
54
+ * heard of is another;
55
+ * - the official connection transport already declared this shell the host
56
+ * owner (`__DSH_TRANSPORT__.ownsHost === true`) AND the page is not a
57
+ * network origin. The official client reads the same hook to derive
58
+ * `connection.isLoopback`, and the desktop shell sets it before any boot
59
+ * entry — without this term the plugin's predicate contradicts the host's
60
+ * own conclusion (#1682). The network-origin guard is deliberate: this
61
+ * plugin's own device-gated landing (a LAN or tunnel page) publishes the
62
+ * same hook to grant a paired remote the full UI, and that page must keep
63
+ * riding the gated channel — `ownsHost` buys the presentation, never an
64
+ * exemption from the pairing fence.
65
+ *
66
+ * The order matters: loopback is checked first so a loopback page keeps its
67
+ * exemption even on a scheme no list carries, and the scheme test runs before
68
+ * the hook test so a web page always reaches the network-origin guard. A
69
+ * missing or empty scheme is not evidence of a local page - the fence stays
70
+ * up - because a real document always reports its own scheme.
71
+ *
72
+ * @param hostname - `location.hostname` of the page.
73
+ * @param protocol - `location.protocol` of the page (for example `https:`).
74
+ * @param transportOwnsHost - `__DSH_TRANSPORT__?.ownsHost` as read by the caller.
75
+ * @returns true when the page is local and needs no remote channel.
76
+ */
77
+ export declare function isLocalPage(hostname: string, protocol?: string, transportOwnsHost?: boolean): boolean;
11
78
  export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
12
79
  /** Connection-plugin method prefix under the gated channel. */
13
80
  export declare const REMOTE_API_PREFIX = "/remote/api";
@@ -38,6 +105,13 @@ export interface RemoteChannelRules {
38
105
  readonly uploadPath: string;
39
106
  /** Page global the pre-Cordis upload hook is published under. */
40
107
  readonly uploadHookGlobal: string;
108
+ /**
109
+ * Schemes that can carry a remotely controlled document (see
110
+ * WEB_PAGE_PROTOCOLS): a page on any other scheme is delivered by an
111
+ * application on this machine. Carried in the rules so the inlined boot
112
+ * script applies the same list.
113
+ */
114
+ readonly webProtocols: readonly string[];
41
115
  /**
42
116
  * Page global carrying the server-issued host-mode grant. Only the plugin's
43
117
  * own device-gated app landing (/pair-app) publishes it; the boot patch
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAyBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
1
+ {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAO/C,CAAA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,OAAO,GAAG,OAAO,CAQrG;AAeD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBA0BlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
@@ -8,6 +8,109 @@
8
8
  /** The gated mirror prefix (must match src/remote-methods.ts). */
9
9
  export const REMOTE_PREFIX = '/remote';
10
10
  import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from "./remote-methods.js";
11
+ /**
12
+ * The schemes that can deliver a page a remote party controls: the network
13
+ * transports, plus the documents a network page can mint. Every other scheme
14
+ * is registered and served by an application on this machine, so its page is
15
+ * the machine's own page whatever the application calls the scheme.
16
+ *
17
+ * The list is deliberately the *web* side rather than an allowlist of known
18
+ * desktop shells. The official DSH Desktop shell serves its Web GUI from
19
+ * `dsh-app://app/` (`location.hostname === 'app'`), which no hostname
20
+ * predicate can recognise; a scheme allowlist fixed that instance (#1682) but
21
+ * left the next shell — or a `file:` page — fenced behind a pairing page it
22
+ * can never complete, because a pairing link is reachable only over the
23
+ * network. Naming the web side instead makes an unknown application scheme
24
+ * local by construction.
25
+ *
26
+ * `blob:`, `data:`, `about:` and `filesystem:` stay on the web side: a
27
+ * network page mints those documents, so they must keep the fence.
28
+ */
29
+ export const WEB_PAGE_PROTOCOLS = [
30
+ 'http:',
31
+ 'https:',
32
+ 'blob:',
33
+ 'data:',
34
+ 'about:',
35
+ 'filesystem:',
36
+ ];
37
+ /**
38
+ * Whether one page scheme can carry a document a remote party controls.
39
+ * @param protocol - `location.protocol` of the page (for example `https:`).
40
+ * @returns true for the network transports and the documents they mint.
41
+ */
42
+ export function isWebPageProtocol(protocol) {
43
+ return WEB_PAGE_PROTOCOLS.includes(protocol);
44
+ }
45
+ /**
46
+ * Hostname-only loopback classification: localhost, the IPv6 loopback literal
47
+ * (WHATWG keeps its brackets) and any 127/8 IPv4 literal. Kept string-in,
48
+ * boolean-out and dependency-free so the browser half and the inlined boot
49
+ * script can both run it verbatim.
50
+ * @param hostname - a page/cookie hostname, IPv6 literals bracketed.
51
+ * @returns true for a loopback name or literal.
52
+ */
53
+ export function isLoopbackHostname(hostname) {
54
+ if (hostname === 'localhost' || hostname === '::1' || hostname === '[::1]')
55
+ return true;
56
+ const parts = hostname.split('.');
57
+ return parts.length === 4 && parts[0] === '127' && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
58
+ }
59
+ /**
60
+ * Whether the current page is served from the machine running the host, so
61
+ * its same-origin traffic must NOT be fenced behind the pairing channel.
62
+ * Three independent facts all describe the page itself and any one is enough:
63
+ *
64
+ * - the page's own hostname is loopback;
65
+ * - the page's protocol is not a {@link WEB_PAGE_PROTOCOLS} web scheme, so an
66
+ * application on this machine delivered it — the desktop shell's
67
+ * `dsh-app://app/` is one such page, and a scheme this build has never
68
+ * heard of is another;
69
+ * - the official connection transport already declared this shell the host
70
+ * owner (`__DSH_TRANSPORT__.ownsHost === true`) AND the page is not a
71
+ * network origin. The official client reads the same hook to derive
72
+ * `connection.isLoopback`, and the desktop shell sets it before any boot
73
+ * entry — without this term the plugin's predicate contradicts the host's
74
+ * own conclusion (#1682). The network-origin guard is deliberate: this
75
+ * plugin's own device-gated landing (a LAN or tunnel page) publishes the
76
+ * same hook to grant a paired remote the full UI, and that page must keep
77
+ * riding the gated channel — `ownsHost` buys the presentation, never an
78
+ * exemption from the pairing fence.
79
+ *
80
+ * The order matters: loopback is checked first so a loopback page keeps its
81
+ * exemption even on a scheme no list carries, and the scheme test runs before
82
+ * the hook test so a web page always reaches the network-origin guard. A
83
+ * missing or empty scheme is not evidence of a local page - the fence stays
84
+ * up - because a real document always reports its own scheme.
85
+ *
86
+ * @param hostname - `location.hostname` of the page.
87
+ * @param protocol - `location.protocol` of the page (for example `https:`).
88
+ * @param transportOwnsHost - `__DSH_TRANSPORT__?.ownsHost` as read by the caller.
89
+ * @returns true when the page is local and needs no remote channel.
90
+ */
91
+ export function isLocalPage(hostname, protocol, transportOwnsHost) {
92
+ if (isLoopbackHostname(hostname))
93
+ return true;
94
+ // A missing or empty scheme is not evidence of a local page: a real
95
+ // document always reports one, so the fence stays up when it is unreadable.
96
+ if (protocol !== undefined && protocol !== '' && !isWebPageProtocol(protocol))
97
+ return true;
98
+ // A granted remote also sets ownsHost; only a page whose own origin is
99
+ // otherwise local may treat the hook as proof of a local page.
100
+ return transportOwnsHost === true && !isNetworkOrigin(hostname);
101
+ }
102
+ /**
103
+ * Whether a hostname can only be reached across a network, so a page on it is
104
+ * remote whatever transport hook it carries: any IPv4/IPv6 literal (a dot or a
105
+ * colon) or any dotted DNS name. A hostname with no dot and no colon is a
106
+ * scheme-local authority (the desktop shell's `app`), which is why this test,
107
+ * not a loopback test, is what the `ownsHost` term is gated on.
108
+ * @param hostname - the page hostname.
109
+ * @returns true when the page cannot be the machine's own local page.
110
+ */
111
+ function isNetworkOrigin(hostname) {
112
+ return hostname.includes('.') || hostname.includes(':');
113
+ }
11
114
  export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
12
115
  /** Connection-plugin method prefix under the gated channel. */
13
116
  export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
@@ -42,6 +145,7 @@ export const REMOTE_CHANNEL_RULES = {
42
145
  deviceQuery: REMOTE_DEVICE_QUERY,
43
146
  uploadPath: '/api/session/uploadFileBinary',
44
147
  uploadHookGlobal: '__DSH_FILE_UPLOAD__',
148
+ webProtocols: WEB_PAGE_PROTOCOLS,
45
149
  hostGrantGlobal: REMOTE_HOST_GRANT_GLOBAL,
46
150
  };
47
151
  /** The window global the boot patch publishes its seat under. */
@@ -149,6 +149,16 @@ export declare function resolveUpdateTarget(deps: {
149
149
  };
150
150
  /** Family children of the anchor: its dependencies under the family scope. */
151
151
  export declare function familyChildren(anchorManifest: Record<string, unknown>): string[];
152
+ /** Concurrent registry probes in flight for one update-status call. */
153
+ export declare const UPDATE_PROBE_CONCURRENCY = 4;
154
+ /**
155
+ * Probe every package's latest version with a bounded number of requests in
156
+ * flight, preserving the caller's order in the result.
157
+ * @param names - package names to probe.
158
+ * @param fetchLatest - the probe for one package (returns undefined on failure).
159
+ * @returns one result per name, in the input order.
160
+ */
161
+ export declare function probeLatestVersions(names: readonly string[], fetchLatest: (name: string) => Promise<string | undefined>): Promise<(string | undefined)[]>;
152
162
  /**
153
163
  * Probe the npm registry for one package's latest release.
154
164
  * @param name - the package name (scope slash URL-encoded).
@@ -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":"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"}
@@ -219,6 +219,30 @@ function familyUpdatePackages(anchor, anchorManifest, profileManifest) {
219
219
  }
220
220
  return [...names];
221
221
  }
222
+ /** Concurrent registry probes in flight for one update-status call. */
223
+ export const UPDATE_PROBE_CONCURRENCY = 4;
224
+ /**
225
+ * Probe every package's latest version with a bounded number of requests in
226
+ * flight, preserving the caller's order in the result.
227
+ * @param names - package names to probe.
228
+ * @param fetchLatest - the probe for one package (returns undefined on failure).
229
+ * @returns one result per name, in the input order.
230
+ */
231
+ export async function probeLatestVersions(names, fetchLatest) {
232
+ const results = new Array(names.length);
233
+ let next = 0;
234
+ const worker = async () => {
235
+ for (;;) {
236
+ const index = next++;
237
+ if (index >= names.length)
238
+ return;
239
+ results[index] = await fetchLatest(names[index]);
240
+ }
241
+ };
242
+ const workers = Math.min(UPDATE_PROBE_CONCURRENCY, names.length);
243
+ await Promise.all(Array.from({ length: workers }, () => worker()));
244
+ return results;
245
+ }
222
246
  /**
223
247
  * Probe the npm registry for one package's latest release.
224
248
  * @param name - the package name (scope slash URL-encoded).
@@ -390,9 +414,14 @@ export async function checkUpdates(deps) {
390
414
  // aggregate) still check every installed @linxin666/* plugin (#377);
391
415
  // familyUpdatePackages skips link:/file: development dependencies.
392
416
  const names = familyUpdatePackages(anchor, manifest, profileManifest);
393
- // The registry probes are independent: run them together instead of
394
- // serializing up to N x 10s of registry latency behind one status call.
395
- const latestList = await Promise.all(names.map(name => deps.fetchLatest(name)));
417
+ // The registry probes are independent, so they run concurrently — but with a
418
+ // small ceiling rather than all at once. A full install fans out ~20 probes,
419
+ // and on hosts behind an application-aware security middlebox that burst is
420
+ // what gets the node process's outbound connections throttled (the update
421
+ // check itself only ever loses latency: one probe is 10s-bounded and a
422
+ // failure is shown as a probe failure). Four keeps the status call quick
423
+ // while staying inside the concurrency such a middlebox tolerates (#1677).
424
+ const latestList = await probeLatestVersions(names, deps.fetchLatest);
396
425
  const packages = [];
397
426
  let probeFailures = 0;
398
427
  names.forEach((name, index) => {