@linxin666/dsh-remote-web-ui 0.3.24 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +13 -10
  3. package/README.zh.md +13 -10
  4. package/icon.svg +3 -0
  5. package/lib/client.js +263 -129
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +565 -256
  8. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  9. package/lib/types/client/FenceNotice.js +0 -3
  10. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +3 -2
  13. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +14 -9
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +13 -4
  17. package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +14 -8
  20. package/lib/types/client/UpdateEntry.js +2 -2
  21. package/lib/types/client/UpdatePanel.js +2 -2
  22. package/lib/types/client/deep-link.js +2 -2
  23. package/lib/types/client/index.d.ts +16 -8
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +47 -18
  26. package/lib/types/client/locales.d.ts +0 -2
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +6 -8
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  30. package/lib/types/client/mobile-adapt.js +103 -22
  31. package/lib/types/client/pair-api.d.ts +32 -3
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +44 -6
  34. package/lib/types/client/plugin-card-seat.d.ts +23 -16
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
  36. package/lib/types/client/plugin-card-seat.js +16 -14
  37. package/lib/types/client/remote-channel.d.ts.map +1 -1
  38. package/lib/types/client/remote-channel.js +18 -5
  39. package/lib/types/client/settings-form.d.ts +17 -17
  40. package/lib/types/client/settings-form.d.ts.map +1 -1
  41. package/lib/types/client/settings-form.js +30 -27
  42. package/lib/types/http.d.ts +18 -1
  43. package/lib/types/http.d.ts.map +1 -1
  44. package/lib/types/http.js +22 -1
  45. package/lib/types/index.d.ts +19 -11
  46. package/lib/types/index.d.ts.map +1 -1
  47. package/lib/types/index.js +91 -76
  48. package/lib/types/inner-auth.d.ts +3 -2
  49. package/lib/types/inner-auth.d.ts.map +1 -1
  50. package/lib/types/inner-auth.js +3 -2
  51. package/lib/types/pair-grant.d.ts +66 -0
  52. package/lib/types/pair-grant.d.ts.map +1 -0
  53. package/lib/types/pair-grant.js +95 -0
  54. package/lib/types/pairing.d.ts +44 -12
  55. package/lib/types/pairing.d.ts.map +1 -1
  56. package/lib/types/pairing.js +69 -20
  57. package/lib/types/pairing.persist.test.js +26 -0
  58. package/lib/types/posture.d.ts +11 -3
  59. package/lib/types/posture.d.ts.map +1 -1
  60. package/lib/types/posture.js +8 -3
  61. package/lib/types/remote-api.d.ts +10 -13
  62. package/lib/types/remote-api.d.ts.map +1 -1
  63. package/lib/types/remote-api.js +34 -34
  64. package/lib/types/remote-channel-boot.d.ts +6 -1
  65. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  66. package/lib/types/remote-channel-boot.js +16 -8
  67. package/lib/types/remote-channel-rules.d.ts +14 -0
  68. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  69. package/lib/types/remote-channel-rules.js +8 -0
  70. package/lib/types/remote-methods.d.ts +1 -1
  71. package/lib/types/remote-methods.d.ts.map +1 -1
  72. package/lib/types/remote-methods.js +1 -0
  73. package/lib/types/routes.d.ts +47 -5
  74. package/lib/types/routes.d.ts.map +1 -1
  75. package/lib/types/routes.js +160 -58
  76. package/lib/types/tunnel.d.ts +21 -6
  77. package/lib/types/tunnel.d.ts.map +1 -1
  78. package/lib/types/tunnel.js +58 -11
  79. package/lib/types/update.d.ts +4 -0
  80. package/lib/types/update.d.ts.map +1 -1
  81. package/lib/types/update.js +74 -26
  82. package/package.json +23 -20
  83. package/src/client/FenceNotice.tsx +0 -2
  84. package/src/client/PluginSettingsCard.tsx +7 -2
  85. package/src/client/RemoteEntry.tsx +14 -9
  86. package/src/client/RemotePanel.tsx +17 -7
  87. package/src/client/RemoteSettingsCard.tsx +16 -9
  88. package/src/client/UpdateEntry.tsx +2 -2
  89. package/src/client/UpdatePanel.tsx +3 -3
  90. package/src/client/deep-link.ts +2 -2
  91. package/src/client/index.ts +61 -27
  92. package/src/client/locales.ts +6 -8
  93. package/src/client/mobile-adapt.ts +97 -20
  94. package/src/client/pair-api.ts +47 -7
  95. package/src/client/plugin-card-seat.ts +24 -17
  96. package/src/client/remote-channel.ts +19 -4
  97. package/src/client/remote.module.css +38 -0
  98. package/src/client/settings-form.ts +34 -31
  99. package/src/http.ts +23 -1
  100. package/src/index.ts +102 -79
  101. package/src/inner-auth.ts +3 -2
  102. package/src/pair-grant.ts +131 -0
  103. package/src/pairing.persist.test.ts +36 -0
  104. package/src/pairing.ts +75 -24
  105. package/src/posture.ts +12 -4
  106. package/src/remote-api.ts +34 -41
  107. package/src/remote-channel-boot.ts +16 -8
  108. package/src/remote-channel-rules.ts +17 -0
  109. package/src/remote-methods.ts +1 -0
  110. package/src/routes.ts +165 -57
  111. package/src/tunnel.ts +56 -11
  112. package/src/update.ts +74 -24
@@ -7,9 +7,9 @@
7
7
  * Security invariants:
8
8
  * - One active token at a time; `issue()` replaces it, so a refreshed QR
9
9
  * immediately invalidates the previous link.
10
- * - A token stays re-usable until it expires or is replaced: the first
11
- * successful `accept()` marks it consumed, but the same link may pair
12
- * again within the window (each re-accept mints a fresh device session).
10
+ * - A token is a bearer credential for its whole window: it is refused only
11
+ * when unknown, expired, or after `stop()`, and the same link may pair
12
+ * repeatedly within that window (each accept mints a fresh device session).
13
13
  * Mobile flows routinely split across cookie contexts (camera preview to
14
14
  * in-app browser to the system browser), and the later context must be
15
15
  * able to complete its own pairing from the same link.
@@ -21,7 +21,7 @@
21
21
  * `idleExpireMs` are deleted on sweep, load, and the next gated request.
22
22
  */
23
23
  import { randomBytes } from 'node:crypto';
24
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
24
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
25
25
  import { dirname } from 'node:path';
26
26
  /**
27
27
  * Default idle-expiry window: 30 days without a heartbeat or a gated
@@ -62,6 +62,7 @@ export const defaultClock = {
62
62
  export class PairingService {
63
63
  config;
64
64
  clock;
65
+ fs;
65
66
  tokens = new Map();
66
67
  devices = new Map();
67
68
  listeners = new Set();
@@ -84,9 +85,10 @@ export class PairingService {
84
85
  * the current one.
85
86
  * @param clock - clock/entropy source (injectable for tests).
86
87
  */
87
- constructor(config, clock = defaultClock) {
88
+ constructor(config, clock = defaultClock, fs = {}) {
88
89
  this.config = config;
89
90
  this.clock = clock;
91
+ this.fs = fs;
90
92
  this.loadPersisted();
91
93
  }
92
94
  /**
@@ -119,7 +121,7 @@ export class PairingService {
119
121
  }
120
122
  this.clampToMaxDevices();
121
123
  if (this.evictIdle())
122
- this.persist();
124
+ this.persistRevocation();
123
125
  }
124
126
  catch {
125
127
  // Unreadable/corrupt: start empty rather than refusing to boot.
@@ -160,7 +162,7 @@ export class PairingService {
160
162
  persist() {
161
163
  const file = this.config.devicesFile;
162
164
  if (file === undefined)
163
- return;
165
+ return true;
164
166
  try {
165
167
  mkdirSync(dirname(file), { recursive: true });
166
168
  const temp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
@@ -170,9 +172,37 @@ export class PairingService {
170
172
  writeFileSync(temp, JSON.stringify(payload), { mode: 0o600 });
171
173
  renameSync(temp, file);
172
174
  this.dirty = false;
175
+ return true;
173
176
  }
174
177
  catch (error) {
175
178
  console.error('remote-web-ui: failed to persist paired devices', error);
179
+ return false;
180
+ }
181
+ }
182
+ /**
183
+ * Persist a table change that REVOKES access (stop, per-device revoke, idle
184
+ * eviction). A failed write would leave the revoked session on disk, and
185
+ * {@link loadPersisted} restores that file verbatim on the next start — the
186
+ * device's still-valid cookie would authorize again after an explicit
187
+ * revocation. When the write cannot be made durable, the stale store is
188
+ * removed instead (a missing file already loads as an empty table), so the
189
+ * failure costs a re-pair rather than silently undoing the revocation.
190
+ */
191
+ persistRevocation() {
192
+ if (this.persist())
193
+ return;
194
+ const file = this.config.devicesFile;
195
+ if (file === undefined)
196
+ return;
197
+ try {
198
+ if (this.fs.removeFile !== undefined)
199
+ this.fs.removeFile(file);
200
+ else
201
+ rmSync(file, { force: true });
202
+ console.error('remote-web-ui: the device revocation could not be persisted; removed the stale device store so a restart cannot restore it');
203
+ }
204
+ catch (error) {
205
+ console.error('remote-web-ui: the device revocation could not be persisted and the stale device store could not be removed', error);
176
206
  }
177
207
  }
178
208
  /** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
@@ -196,9 +226,14 @@ export class PairingService {
196
226
  get publicBaseUrl() {
197
227
  return this.publicBase;
198
228
  }
199
- /** Set or clear the public base URL (a tunnel in front of this server). */
229
+ /**
230
+ * Set or clear the public base URL (a tunnel in front of this server). The
231
+ * value is canonicalized to its origin: a trailing slash (what a browser
232
+ * address-bar copy produces) or a path would mint a dead `//pair-accept`
233
+ * link, which WHATWG resolves as an authority rather than a path.
234
+ */
200
235
  setPublicBaseUrl(url) {
201
- this.publicBase = url;
236
+ this.publicBase = url === undefined ? undefined : canonicalBaseUrl(url);
202
237
  this.notify();
203
238
  }
204
239
  /** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
@@ -244,7 +279,6 @@ export class PairingService {
244
279
  id: `t${this.tokenSerial}`,
245
280
  issuedAt: now,
246
281
  expiresAt: now + this.config.tokenTtlMs,
247
- consumed: false,
248
282
  ...(workspaceId !== undefined ? { workspaceId } : {}),
249
283
  ...(address !== undefined ? { address } : {}),
250
284
  });
@@ -252,21 +286,20 @@ export class PairingService {
252
286
  return { token, expiresAt: now + this.config.tokenTtlMs };
253
287
  }
254
288
  /**
255
- * Consume a token and bind a device session. One-time: the second
256
- * successful call for the same token is impossible because the first
257
- * consumes it.
289
+ * Redeem a token and bind a device session. The token is a bearer
290
+ * credential for its whole window, not a single-use nonce: it is refused
291
+ * only when unknown, past its expiry, or after stop(), and every successful
292
+ * call — including a repeat within the same window — mints a fresh device
293
+ * session (see the module doc for the mobile cookie-context rationale).
258
294
  * @param token - the token secret from the QR link.
259
295
  * @param userAgent - optional User-Agent header captured at accept.
260
296
  * @returns the new device id, or a refusal code.
261
297
  */
262
298
  accept(token, userAgent) {
263
299
  const record = this.tokens.get(token);
264
- // A consumed token stays a valid bearer credential until expiry or
265
- // replacement (see the module doc): re-accept mints a fresh device.
266
300
  if (record === undefined || this.stopped || this.clock.now() > record.expiresAt) {
267
301
  return { ok: false, code: 'invalid' };
268
302
  }
269
- record.consumed = true;
270
303
  const deviceId = this.clock.randomToken();
271
304
  const now = this.clock.now();
272
305
  if (this.devices.size >= this.config.maxDevices) {
@@ -297,7 +330,7 @@ export class PairingService {
297
330
  stop() {
298
331
  this.tokens.clear();
299
332
  this.devices.clear();
300
- this.persist();
333
+ this.persistRevocation();
301
334
  this.stopped = true;
302
335
  this.notify();
303
336
  }
@@ -312,7 +345,7 @@ export class PairingService {
312
345
  return false;
313
346
  if (!this.devices.delete(deviceId))
314
347
  return false;
315
- this.persist();
348
+ this.persistRevocation();
316
349
  this.notify();
317
350
  return true;
318
351
  }
@@ -349,7 +382,9 @@ export class PairingService {
349
382
  */
350
383
  sweep() {
351
384
  const evicted = this.evictIdle();
352
- if (evicted || this.dirty)
385
+ if (evicted)
386
+ this.persistRevocation();
387
+ else if (this.dirty)
353
388
  this.persist();
354
389
  this.notify();
355
390
  }
@@ -407,7 +442,7 @@ export class PairingService {
407
442
  const limit = this.config.idleExpireMs ?? DEFAULT_IDLE_EXPIRE_MS;
408
443
  if (this.clock.now() - session.lastSeenAt > limit) {
409
444
  this.devices.delete(deviceId);
410
- this.persist();
445
+ this.persistRevocation();
411
446
  this.notify();
412
447
  return undefined;
413
448
  }
@@ -494,6 +529,20 @@ function relayEqual(a, b) {
494
529
  function sameStrings(a, b) {
495
530
  return a.length === b.length && a.every((value, index) => value === b[index]);
496
531
  }
532
+ /**
533
+ * Canonical origin of a public base URL. An unparsable value is returned
534
+ * verbatim so the caller's malformed-base warning stays the single owner of
535
+ * that case.
536
+ * @param url - the configured public base.
537
+ */
538
+ export function canonicalBaseUrl(url) {
539
+ try {
540
+ return new URL(url).origin;
541
+ }
542
+ catch {
543
+ return url;
544
+ }
545
+ }
497
546
  /** Strip control characters and cap the User-Agent stored with a session. */
498
547
  export function sanitizeUserAgent(raw) {
499
548
  if (raw === undefined)
@@ -180,4 +180,30 @@ describe('PairingService device persistence', () => {
180
180
  const deviceId = result.ok ? result.deviceId : '';
181
181
  expect(saved[deviceId]?.userAgent).toBe('Mozilla/5.0 TestPhone');
182
182
  });
183
+ it('operator is not left with a revocation that a restart silently undoes', () => {
184
+ // Given a device store that cannot be written (its parent path is a file)
185
+ // while a stale, readable store sits beside it.
186
+ const blocker = join(dir, 'blocker');
187
+ writeFileSync(blocker, 'not a directory');
188
+ const file = join(blocker, 'devices.json');
189
+ const removed = [];
190
+ const service = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock(), { removeFile: (path) => { removed.push(path); } });
191
+ // When the operator stops remote access and the write cannot be made durable.
192
+ service.stop();
193
+ // Then the stale store is handed to the removal step, so a restart cannot
194
+ // restore the revoked sessions from disk.
195
+ expect(removed).toEqual([file]);
196
+ });
197
+ it('operator keeps the device store when a revocation persist succeeds', () => {
198
+ // Given a writable device store with one paired device.
199
+ const file = join(dir, 'devices.json');
200
+ const removed = [];
201
+ const service = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock(), { removeFile: (path) => { removed.push(path); } });
202
+ const deviceId = pairDevice(service);
203
+ // When the device is revoked and the write lands.
204
+ expect(service.revoke(deviceId)).toBe(true);
205
+ // Then the store stays (with the row gone) and nothing is removed.
206
+ expect(removed).toEqual([]);
207
+ expect(JSON.parse(readFileSync(file, 'utf8'))).toEqual({});
208
+ });
183
209
  });
@@ -9,15 +9,23 @@
9
9
  *
10
10
  * The probe issues loopback requests with a forged Host header — the exact
11
11
  * shape a tunnel or LAN client produces — and treats anything other than a
12
- * 403 as exposed: the fence is documented to refuse with 403, so any other
13
- * status means the request reached the RPC bridge.
12
+ * 403 or a 401 as exposed. The fence is documented to refuse with 403; the
13
+ * harness answers 401 when the Host fence PASSED but the browser-auth cookie
14
+ * is missing (the connection plugin checks the fence first and the cookie
15
+ * second). A 401 therefore means the /api surface is still gated — reporting
16
+ * it as open produced a false CRITICAL on exactly the supported LAN
17
+ * deployment (0.0.0.0 bind auto-trusts the LAN literals) and desensitized the
18
+ * one alarm that should mean a real exposure.
14
19
  */
15
20
  import type { ClientRequest, ClientRequestArgs } from 'node:http';
16
21
  /** One probed origin. */
17
22
  export interface PostureHost {
18
23
  /** The forged Host header value that was probed. */
19
24
  host: string;
20
- /** True when the probe reached past the fence (anything but 403). */
25
+ /**
26
+ * True when the probe reached the RPC bridge without any gate refusing it
27
+ * (a status that is neither the fence's 403 nor the browser-auth 401).
28
+ */
21
29
  exposed: boolean;
22
30
  }
23
31
  /** One probe round. */
@@ -1 +1 @@
1
- {"version":3,"file":"posture.d.ts","sourceRoot":"","sources":["../../src/posture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAGjE,yBAAyB;AACzB,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,uBAAuB;AACvB,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,2CAA2C;IAC3C,KAAK,EAAE,WAAW,EAAE,CAAA;CACrB;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAE7D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAehH;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,KAAK,aAAa,CAAA;AAyC5G,qBAAqB;AACrB,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CAWzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAGxG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEpG"}
1
+ {"version":3,"file":"posture.d.ts","sourceRoot":"","sources":["../../src/posture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAGjE,yBAAyB;AACzB,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,uBAAuB;AACvB,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,2CAA2C;IAC3C,KAAK,EAAE,WAAW,EAAE,CAAA;CACrB;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAE7D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAehH;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,KAAK,aAAa,CAAA;AAyC5G,qBAAqB;AACrB,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CAWzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAGxG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEpG"}
@@ -9,8 +9,13 @@
9
9
  *
10
10
  * The probe issues loopback requests with a forged Host header — the exact
11
11
  * shape a tunnel or LAN client produces — and treats anything other than a
12
- * 403 as exposed: the fence is documented to refuse with 403, so any other
13
- * status means the request reached the RPC bridge.
12
+ * 403 or a 401 as exposed. The fence is documented to refuse with 403; the
13
+ * harness answers 401 when the Host fence PASSED but the browser-auth cookie
14
+ * is missing (the connection plugin checks the fence first and the cookie
15
+ * second). A 401 therefore means the /api surface is still gated — reporting
16
+ * it as open produced a false CRITICAL on exactly the supported LAN
17
+ * deployment (0.0.0.0 bind auto-trusts the LAN literals) and desensitized the
18
+ * one alarm that should mean a real exposure.
14
19
  */
15
20
  import http from 'node:http';
16
21
  /** Whether any probed origin is currently exposed. */
@@ -75,7 +80,7 @@ async function probeHost(port, hostHeader, request, timeoutMs) {
75
80
  path: '/api/session.list',
76
81
  headers: { host: hostHeader, 'content-type': 'application/json' },
77
82
  timeout: timeoutMs,
78
- }, status => { finish(status !== 403); });
83
+ }, status => { finish(status !== 403 && status !== 401); });
79
84
  const timer = setTimeout(() => { finish(false); }, timeoutMs + 1_000);
80
85
  handle.on('error', () => { finish(false); });
81
86
  handle.end('{}');
@@ -7,10 +7,14 @@
7
7
  * consult.
8
8
  *
9
9
  * Security model:
10
- * - While `requirePairingForLan` is on (default), every request must carry a
11
- * live paired-device cookie, enforced before any bytes are forwarded and
12
- * before any host call. With the policy off, the cookie gate is skipped
13
- * (the local-only denials below still apply).
10
+ * - Every request must carry a live paired-device credential, enforced before
11
+ * any bytes are forwarded and before any host call. This gate is
12
+ * unconditional and does NOT follow `requirePairingForLan`: the channel
13
+ * re-issues traffic with the process's own browser-auth credential, so it is
14
+ * an authentication amplifier — an unpaired caller admitted here would reach
15
+ * the full host API without ever holding that credential. The LAN policy
16
+ * governs the plain `/api` surface only (whether the harness fence's LAN
17
+ * auto-trust stands on its own); it can never widen this channel.
14
18
  * - A paired remote desktop is a full-control credential: the browser half
15
19
  * flips the official UI into host mode (the transport ownsHost hook), so
16
20
  * the configuration plane (settings, credentials, presets, deliverables)
@@ -31,15 +35,8 @@ export { REMOTE_API_PREFIX } from './remote-methods.ts';
31
35
  export interface RemoteApiDeps {
32
36
  /** The pairing service (device gate + cookie name). */
33
37
  service: PairingService;
34
- /** The local webServer port the loopback proxy connects to. */
38
+ /** Local webServer port the loopback proxy connects to. */
35
39
  port: number;
36
- /**
37
- * Live policy: whether the paired-device cookie gates the /remote channel.
38
- * When false, requests are proxied without a cookie (the loopback-only
39
- * denials still apply). A function is re-read per request, so a settings
40
- * edit takes effect without a restart. Defaults to true.
41
- */
42
- requirePairingForLan?: boolean | (() => boolean);
43
40
  /**
44
41
  * The process's inner browser-auth credential attached to re-issued
45
42
  * requests (the connection plugin's /api route enforces that cookie and
@@ -68,7 +65,7 @@ export declare function loopbackOnlyDenial(innerPath: string): string | undefine
68
65
  export declare function pairedDeviceIdOf(req: IncomingMessage, service: PairingService): string | undefined;
69
66
  /**
70
67
  * Build the remote desktop channel HTTP routes.
71
- * @param deps - pairing service + local port + live pairing policy.
68
+ * @param deps - pairing service + local port + inner credential.
72
69
  * @returns the routes to register on webServer.
73
70
  */
74
71
  export declare function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[];
@@ -1 +1 @@
1
- {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;IAChD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA0CnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CA2CjF"}
1
+ {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAwCnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CAyCjF"}
@@ -7,10 +7,14 @@
7
7
  * consult.
8
8
  *
9
9
  * Security model:
10
- * - While `requirePairingForLan` is on (default), every request must carry a
11
- * live paired-device cookie, enforced before any bytes are forwarded and
12
- * before any host call. With the policy off, the cookie gate is skipped
13
- * (the local-only denials below still apply).
10
+ * - Every request must carry a live paired-device credential, enforced before
11
+ * any bytes are forwarded and before any host call. This gate is
12
+ * unconditional and does NOT follow `requirePairingForLan`: the channel
13
+ * re-issues traffic with the process's own browser-auth credential, so it is
14
+ * an authentication amplifier — an unpaired caller admitted here would reach
15
+ * the full host API without ever holding that credential. The LAN policy
16
+ * governs the plain `/api` surface only (whether the harness fence's LAN
17
+ * auto-trust stands on its own); it can never widen this channel.
14
18
  * - A paired remote desktop is a full-control credential: the browser half
15
19
  * flips the official UI into host mode (the transport ownsHost hook), so
16
20
  * the configuration plane (settings, credentials, presets, deliverables)
@@ -93,23 +97,21 @@ export function pairedDeviceIdOf(req, service) {
93
97
  }
94
98
  /**
95
99
  * Build the remote desktop channel HTTP routes.
96
- * @param deps - pairing service + local port + live pairing policy.
100
+ * @param deps - pairing service + local port + inner credential.
97
101
  * @returns the routes to register on webServer.
98
102
  */
99
103
  export function makeRemoteApiRoutes(deps) {
100
- const { service, port, requirePairingForLan = true } = deps;
104
+ const { service, port } = deps;
101
105
  const handler = (req, res) => {
102
- // Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
103
- // With the live policy off, untrusted-but-policy-open callers are proxied
104
- // (a stale client rewrite must not 403); loopback-only denials stay below.
105
- const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
106
- if (require) {
107
- const paired = pairedDeviceIdOf(req, service);
108
- if (paired === undefined) {
109
- req.resume();
110
- envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
111
- return;
112
- }
106
+ // Paired credential first — before any bytes are buffered or forwarded.
107
+ // Unconditional by design: this channel attaches the process's own
108
+ // browser-auth cookie to every re-issued call, so admitting an unpaired
109
+ // caller would hand out that credential's authority (issue #1665).
110
+ const paired = pairedDeviceIdOf(req, service);
111
+ if (paired === undefined) {
112
+ req.resume();
113
+ envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
114
+ return;
113
115
  }
114
116
  const method = req.method ?? 'GET';
115
117
  if (!ALLOWED_METHODS.has(method)) {
@@ -159,24 +161,22 @@ export function upgradeInnerPath(reqUrl, fallbackPath) {
159
161
  * @returns the upgrade routes to register on webServer.
160
162
  */
161
163
  export function makeRemoteApiUpgradeRoutes(deps) {
162
- const { service, port, requirePairingForLan = true } = deps;
164
+ const { service, port } = deps;
163
165
  const handlerFor = (fallbackPath) => (req, socket, head) => {
164
- const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
165
- if (require) {
166
- // WebSocket handshakes cannot carry headers from the Web API, so the
167
- // cookieless credential rides the query; the cookie stays the primary.
168
- let queryDevice;
169
- try {
170
- queryDevice = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get(REMOTE_DEVICE_QUERY) ?? undefined;
171
- }
172
- catch { /* fall through to the cookie */ }
173
- const deviceId = pairedDeviceIdOf(req, service);
174
- const paired = deviceId ?? (queryDevice !== undefined && service.touchDevice(queryDevice) ? queryDevice : undefined);
175
- if (paired === undefined) {
176
- socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
177
- socket.destroy();
178
- return;
179
- }
166
+ // WebSocket handshakes cannot carry headers from the Web API, so the
167
+ // cookieless credential rides the query; the cookie stays the primary.
168
+ // The gate is unconditional here too (issue #1665).
169
+ let queryDevice;
170
+ try {
171
+ queryDevice = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get(REMOTE_DEVICE_QUERY) ?? undefined;
172
+ }
173
+ catch { /* fall through to the cookie */ }
174
+ const deviceId = pairedDeviceIdOf(req, service);
175
+ const paired = deviceId ?? (queryDevice !== undefined && service.touchDevice(queryDevice) ? queryDevice : undefined);
176
+ if (paired === undefined) {
177
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
178
+ socket.destroy();
179
+ return;
180
180
  }
181
181
  const inner = upgradeInnerPath(req.url, fallbackPath);
182
182
  const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner);
@@ -17,7 +17,12 @@
17
17
  * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
18
18
  * connection plugin reads it: the paired remote desktop gets the full
19
19
  * settings/credentials/presets surface, and every call still rides the
20
- * gated /remote channel. It finally publishes the official pre-Cordis
20
+ * gated /remote channel. Host mode is server-granted, not origin-asserted:
21
+ * the hook is installed only when the device-gated app landing (/pair-app)
22
+ * published the grant marker (REMOTE_HOST_GRANT_GLOBAL) ahead of this
23
+ * script, so a shell served to an unpaired browser - a fence-open
24
+ * deployment - never presents itself as the machine owner. It finally
25
+ * publishes the official pre-Cordis
21
26
  * upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
22
27
  * transport otherwise runs inside a Web Worker whose own globals the
23
28
  * main-thread rewrite cannot reach (issue #1580). The script self-skips on
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAqJrG;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,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"}
@@ -17,7 +17,12 @@
17
17
  * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
18
18
  * connection plugin reads it: the paired remote desktop gets the full
19
19
  * settings/credentials/presets surface, and every call still rides the
20
- * gated /remote channel. It finally publishes the official pre-Cordis
20
+ * gated /remote channel. Host mode is server-granted, not origin-asserted:
21
+ * the hook is installed only when the device-gated app landing (/pair-app)
22
+ * published the grant marker (REMOTE_HOST_GRANT_GLOBAL) ahead of this
23
+ * script, so a shell served to an unpaired browser - a fence-open
24
+ * deployment - never presents itself as the machine owner. It finally
25
+ * publishes the official pre-Cordis
21
26
  * upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
22
27
  * transport otherwise runs inside a Web Worker whose own globals the
23
28
  * main-thread rewrite cannot reach (issue #1580). The script self-skips on
@@ -38,14 +43,17 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
38
43
  return '(function(){' +
39
44
  'try{' +
40
45
  'var w=window,loc=w.location,h=loc.hostname;' +
41
- // Loopback origins keep the original paths (mirrors isLoopbackHostname).
42
- "if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
43
- // Host mode: the paired remote desktop presents itself as the machine
44
- // owner, so the official UI keeps its full configuration surface (the
45
- // settings mirror, document controller, and deliverables open actions
46
- // all branch on connection.isLoopback). Must run before any boot entry.
47
- 'try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}' +
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;" +
48
49
  'var R=' + json + ';' +
50
+ // Host mode is server-granted: only the device-gated app landing
51
+ // publishes the grant marker (in a capture script that runs ahead of
52
+ // this one), so a shell that merely sits on a non-loopback origin - an
53
+ // unpaired browser reaching a fence-open deployment - keeps the official
54
+ // UI's memory-scope presentation instead of posing as the machine owner.
55
+ // Must run before any boot entry.
56
+ 'try{if(w[R.hostGrantGlobal]===true){if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}}catch(e){}' +
49
57
  // The cookieless device credential: read lazily per call - the
50
58
  // /pair-app capture script sets it in head AFTER this boot script ran,
51
59
  // so a parse-time read would always see null.
@@ -11,6 +11,12 @@ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from './remote-methods.ts';
11
11
  export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
12
12
  /** Connection-plugin method prefix under the gated channel. */
13
13
  export declare const REMOTE_API_PREFIX = "/remote/api";
14
+ /**
15
+ * The window global the device-gated app landing publishes to grant host mode.
16
+ * Set by the /pair-app capture script, read by the parse-time boot patch: the
17
+ * transport hook (ownsHost) is server-granted, not asserted from the origin.
18
+ */
19
+ export declare const REMOTE_HOST_GRANT_GLOBAL = "__DSH_REMOTE_HOST_GRANT__";
14
20
  /** Every decision input of the remote-channel rewrite, JSON-serializable. */
15
21
  export interface RemoteChannelRules {
16
22
  readonly remotePrefix: string;
@@ -32,6 +38,14 @@ export interface RemoteChannelRules {
32
38
  readonly uploadPath: string;
33
39
  /** Page global the pre-Cordis upload hook is published under. */
34
40
  readonly uploadHookGlobal: string;
41
+ /**
42
+ * Page global carrying the server-issued host-mode grant. Only the plugin's
43
+ * own device-gated app landing (/pair-app) publishes it; the boot patch
44
+ * therefore enables the official UI's host mode only for a shell the server
45
+ * actually granted, never for one that merely happens to sit on a
46
+ * non-loopback origin.
47
+ */
48
+ readonly hostGrantGlobal: string;
35
49
  }
36
50
  /** The live rule set. */
37
51
  export declare const REMOTE_CHANNEL_RULES: RemoteChannelRules;
@@ -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,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;CAClC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAuBlC,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,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"}
@@ -11,6 +11,12 @@ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from "./remote-methods.js";
11
11
  export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
12
12
  /** Connection-plugin method prefix under the gated channel. */
13
13
  export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
14
+ /**
15
+ * The window global the device-gated app landing publishes to grant host mode.
16
+ * Set by the /pair-app capture script, read by the parse-time boot patch: the
17
+ * transport hook (ownsHost) is server-granted, not asserted from the origin.
18
+ */
19
+ export const REMOTE_HOST_GRANT_GLOBAL = '__DSH_REMOTE_HOST_GRANT__';
14
20
  /** The live rule set. */
15
21
  export const REMOTE_CHANNEL_RULES = {
16
22
  remotePrefix: REMOTE_PREFIX,
@@ -28,6 +34,7 @@ export const REMOTE_CHANNEL_RULES = {
28
34
  '/api/remote.mux',
29
35
  '/sidebar/ws/terminal',
30
36
  '/sidebar/ws/agent-terminals',
37
+ '/sidebar/ws/agent-opens',
31
38
  '/api/dsh-ssh/terminal',
32
39
  ],
33
40
  deviceHeader: REMOTE_DEVICE_HEADER,
@@ -35,6 +42,7 @@ export const REMOTE_CHANNEL_RULES = {
35
42
  deviceQuery: REMOTE_DEVICE_QUERY,
36
43
  uploadPath: '/api/session/uploadFileBinary',
37
44
  uploadHookGlobal: '__DSH_FILE_UPLOAD__',
45
+ hostGrantGlobal: REMOTE_HOST_GRANT_GLOBAL,
38
46
  };
39
47
  /** The window global the boot patch publishes its seat under. */
40
48
  export const REMOTE_CHANNEL_BOOT_GLOBAL = '__DSH_REMOTE_CHANNEL_BOOT__';
@@ -31,7 +31,7 @@ export declare const REMOTE_API_PATHS: {
31
31
  * Exact upgrade paths registered on webServer (the SDK matches upgrades by
32
32
  * exact path, not prefix). Query strings ride on the request URL.
33
33
  */
34
- export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/remote.mux", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/api/dsh-ssh/terminal"];
34
+ export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/remote.mux", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/sidebar/ws/agent-opens", "/remote/api/dsh-ssh/terminal"];
35
35
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
36
36
  export declare const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
37
37
  /** Family settings-bridge HTTP prefix — re-exposed to paired devices (plain settings parity). */
@@ -1 +1 @@
1
- {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,0IAKvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
1
+ {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,4KAMvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
@@ -35,6 +35,7 @@ export const REMOTE_UPGRADE_PATHS = [
35
35
  REMOTE_API_PATHS.mux,
36
36
  `${REMOTE_PREFIX}/sidebar/ws/terminal`,
37
37
  `${REMOTE_PREFIX}/sidebar/ws/agent-terminals`,
38
+ `${REMOTE_PREFIX}/sidebar/ws/agent-opens`,
38
39
  `${REMOTE_API_PREFIX}/dsh-ssh/terminal`,
39
40
  ];
40
41
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */