dsh-relay-phone 0.18.0 → 0.18.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.
- package/package.json +1 -1
- package/src/boot-source.js +20 -2
- package/src/pump.js +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-relay-phone",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Remote-control your local DeepSeek Harness from a phone through your own zero-knowledge relay: phone-first page, QR pairing, end-to-end encryption, approvals, full desktop UI fallback.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/boot-source.js
CHANGED
|
@@ -246,6 +246,24 @@ export function buildBootSource(id, gate) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
var BOUND = 0x02
|
|
249
|
+
|
|
250
|
+
// The target the host will see, not the URL the browser resolved.
|
|
251
|
+
//
|
|
252
|
+
// DSH calls relative URLs (for example api/session/list), which resolve against
|
|
253
|
+
// the page at /c/<id>/ and come back with the relay prefix attached. The host
|
|
254
|
+
// strips that prefix before comparing, so the sealed value must be stripped
|
|
255
|
+
// too - a prefix the client adds and the host removes is a guaranteed mismatch.
|
|
256
|
+
function boundTarget(absolute) {
|
|
257
|
+
var path = absolute.pathname
|
|
258
|
+
if (PREFIX && path.indexOf(PREFIX) === 0) path = path.slice(PREFIX.length) || '/'
|
|
259
|
+
var search = absolute.search
|
|
260
|
+
if (search) {
|
|
261
|
+
var parts = search.slice(1).split('&').filter(function (part) { return part.indexOf('g=') !== 0 })
|
|
262
|
+
search = parts.length > 0 && parts[0] !== '' ? '?' + parts.join('&') : ''
|
|
263
|
+
}
|
|
264
|
+
return path + search
|
|
265
|
+
}
|
|
266
|
+
|
|
249
267
|
function sealBound(method, target, contentType, bytes) {
|
|
250
268
|
var enc = new TextEncoder()
|
|
251
269
|
var header = enc.encode(JSON.stringify({ m: method, t: target, c: contentType || '' }))
|
|
@@ -385,7 +403,7 @@ export function buildBootSource(id, gate) {
|
|
|
385
403
|
// with the host cookie.
|
|
386
404
|
headers.set('x-relay-bind', bytesToBase64(sealBound(
|
|
387
405
|
options.method || 'GET',
|
|
388
|
-
absolute
|
|
406
|
+
boundTarget(absolute),
|
|
389
407
|
'',
|
|
390
408
|
new Uint8Array(8).fill(0),
|
|
391
409
|
)))
|
|
@@ -404,7 +422,7 @@ export function buildBootSource(id, gate) {
|
|
|
404
422
|
// Bind the body to the request it was made for: method, path+query and
|
|
405
423
|
// content-type travel inside the envelope, so the relay cannot re-route
|
|
406
424
|
// a captured request to another endpoint.
|
|
407
|
-
body: sealBound(options.method || 'GET', absolute
|
|
425
|
+
body: sealBound(options.method || 'GET', boundTarget(absolute), contentType, bytes),
|
|
408
426
|
})).then(maybeUnwrap)
|
|
409
427
|
}).catch(function (error) {
|
|
410
428
|
// A body-serialization failure must not silently downgrade to plaintext.
|
package/src/pump.js
CHANGED
|
@@ -721,7 +721,11 @@ export class PumpManager {
|
|
|
721
721
|
return index === -1 ? [value, ''] : [value.slice(0, index), value.slice(index + 1)]
|
|
722
722
|
}
|
|
723
723
|
const [outerPath, outerQuery] = splitTarget(info.target ?? info.path)
|
|
724
|
-
|
|
724
|
+
// A browser that resolved a relative URL against the page seals the relay
|
|
725
|
+
// prefix too (`/c/<id>/api/x`); strip it the same way the router does. The
|
|
726
|
+
// sealed value still names the method and path, so this removes no authority.
|
|
727
|
+
const sealedTarget = String(bound.t).replace(/^\/c\/[A-Za-z0-9]{4,16}(?=\/|$)/, '') || '/'
|
|
728
|
+
const [innerPath, innerQuery] = splitTarget(sealedTarget)
|
|
725
729
|
if (outerPath !== innerPath) {
|
|
726
730
|
return `path ${outerPath.slice(0, 60)} != sealed ${innerPath.slice(0, 60)}`
|
|
727
731
|
}
|
|
@@ -988,6 +992,10 @@ export class PumpManager {
|
|
|
988
992
|
upstream.setNoDelay(true)
|
|
989
993
|
upstream.on('connect', () => {
|
|
990
994
|
upstream.write(out)
|
|
995
|
+
// Frames are NOT flushed here: the host has not answered 101 yet, and a
|
|
996
|
+
// byte written before the switch looks like a pipelined HTTP request to
|
|
997
|
+
// it. Whether the browser's first frame beat the 101 was a race, which is
|
|
998
|
+
// why the mux sometimes sat silent and the page then retried.
|
|
991
999
|
this.pumpPhoneWs(sid, stream)
|
|
992
1000
|
})
|
|
993
1001
|
upstream.on('data', (data) => {
|
|
@@ -1001,6 +1009,8 @@ export class PumpManager {
|
|
|
1001
1009
|
this.client?.sendStream(sid, stream.wsServerBuf.subarray(0, idx + 4))
|
|
1002
1010
|
stream.wsServerBuf = stream.wsServerBuf.subarray(idx + 4)
|
|
1003
1011
|
stream.wsHandshakeDone = true
|
|
1012
|
+
// The switch is real now: release whatever the phone already sent.
|
|
1013
|
+
this.pumpPhoneWs(sid, stream)
|
|
1004
1014
|
}
|
|
1005
1015
|
this.pumpServerWs(sid, stream)
|
|
1006
1016
|
})
|
|
@@ -1080,6 +1090,8 @@ export class PumpManager {
|
|
|
1080
1090
|
// -- websocket sealing ---------------------------------------------------------------
|
|
1081
1091
|
pumpPhoneWs(sid, stream) {
|
|
1082
1092
|
if (!stream.upstream || stream.upstream.destroyed) return
|
|
1093
|
+
// Not before the host answered 101: see the comment at the upgrade handler.
|
|
1094
|
+
if (!stream.wsHandshakeDone) return
|
|
1083
1095
|
for (;;) {
|
|
1084
1096
|
const parsed = parseFrame(stream.wsPhoneBuf)
|
|
1085
1097
|
if (!parsed) return
|