@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.
- package/README.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/icon.svg +3 -0
- package/lib/client.js +263 -129
- package/lib/client.js.map +1 -1
- package/lib/index.js +565 -256
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +0 -3
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +14 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +13 -4
- package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +14 -8
- package/lib/types/client/UpdateEntry.js +2 -2
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.d.ts +16 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +47 -18
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +103 -22
- package/lib/types/client/pair-api.d.ts +32 -3
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +44 -6
- package/lib/types/client/plugin-card-seat.d.ts +23 -16
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
- package/lib/types/client/plugin-card-seat.js +16 -14
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +18 -5
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +19 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +91 -76
- package/lib/types/inner-auth.d.ts +3 -2
- package/lib/types/inner-auth.d.ts.map +1 -1
- package/lib/types/inner-auth.js +3 -2
- package/lib/types/pair-grant.d.ts +66 -0
- package/lib/types/pair-grant.d.ts.map +1 -0
- package/lib/types/pair-grant.js +95 -0
- package/lib/types/pairing.d.ts +44 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +69 -20
- package/lib/types/pairing.persist.test.js +26 -0
- package/lib/types/posture.d.ts +11 -3
- package/lib/types/posture.d.ts.map +1 -1
- package/lib/types/posture.js +8 -3
- package/lib/types/remote-api.d.ts +10 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +34 -34
- package/lib/types/remote-channel-boot.d.ts +6 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +16 -8
- package/lib/types/remote-channel-rules.d.ts +14 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +8 -0
- package/lib/types/remote-methods.d.ts +1 -1
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -0
- package/lib/types/routes.d.ts +47 -5
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +160 -58
- package/lib/types/tunnel.d.ts +21 -6
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +58 -11
- package/lib/types/update.d.ts +4 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +74 -26
- package/package.json +23 -20
- package/src/client/FenceNotice.tsx +0 -2
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/RemoteEntry.tsx +14 -9
- package/src/client/RemotePanel.tsx +17 -7
- package/src/client/RemoteSettingsCard.tsx +16 -9
- package/src/client/UpdateEntry.tsx +2 -2
- package/src/client/UpdatePanel.tsx +3 -3
- package/src/client/deep-link.ts +2 -2
- package/src/client/index.ts +61 -27
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +97 -20
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +24 -17
- package/src/client/remote-channel.ts +19 -4
- package/src/client/remote.module.css +38 -0
- package/src/client/settings-form.ts +34 -31
- package/src/http.ts +23 -1
- package/src/index.ts +102 -79
- package/src/inner-auth.ts +3 -2
- package/src/pair-grant.ts +131 -0
- package/src/pairing.persist.test.ts +36 -0
- package/src/pairing.ts +75 -24
- package/src/posture.ts +12 -4
- package/src/remote-api.ts +34 -41
- package/src/remote-channel-boot.ts +16 -8
- package/src/remote-channel-rules.ts +17 -0
- package/src/remote-methods.ts +1 -0
- package/src/routes.ts +165 -57
- package/src/tunnel.ts +56 -11
- package/src/update.ts +74 -24
package/lib/types/pairing.js
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
});
|
package/lib/types/posture.d.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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"}
|
package/lib/types/posture.js
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
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 +
|
|
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
|
|
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"}
|
package/lib/types/remote-api.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* consult.
|
|
8
8
|
*
|
|
9
9
|
* Security model:
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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 +
|
|
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
|
|
104
|
+
const { service, port } = deps;
|
|
101
105
|
const handler = (req, res) => {
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
164
|
+
const { service, port } = deps;
|
|
163
165
|
const handlerFor = (fallbackPath) => (req, socket, head) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
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,
|
|
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. */
|