dsh-relay-phone 0.18.2 → 0.18.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-relay-phone",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
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",
@@ -472,32 +472,44 @@ export function buildBootSource(id, gate) {
472
472
  try { userHandlers[i](ev) } catch (e) { console.error(e) }
473
473
  }
474
474
  }
475
- function deliverSealed(b64) {
475
+ // Binary is a real message kind here: the client sets binaryType="arraybuffer"
476
+ // and the account avatar arrives that way. Decoding those bytes as text both
477
+ // corrupts them and makes the host reject the frame, which is what killed the
478
+ // mux when the account panel opened.
479
+ function deliverSealed(b64, isBinary) {
476
480
  var bin = atob(b64)
477
481
  var bytes = new Uint8Array(bin.length)
478
482
  for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
479
483
  var plain = openEnvelope(bytes)
480
484
  if (!plain) return
485
+ if (isBinary) {
486
+ var copy = new Uint8Array(plain.length)
487
+ copy.set(plain)
488
+ return deliverRaw(copy.buffer)
489
+ }
481
490
  deliverRaw(new TextDecoder().decode(plain))
482
491
  }
483
492
  target.addEventListener('message', function (ev) {
484
493
  if (typeof ev.data !== 'string') return deliverRaw(ev.data)
485
494
  try {
486
495
  var parsed = JSON.parse(ev.data)
487
- if (parsed && typeof parsed.__relay === 'string') return deliverSealed(parsed.__relay)
496
+ if (parsed && typeof parsed.__relay === 'string') {
497
+ return deliverSealed(parsed.__relay, parsed.__bin === 1)
498
+ }
488
499
  } catch (e) {}
489
500
  deliverRaw(ev.data)
490
501
  })
491
502
 
492
503
  var origSend = target.send.bind(target)
493
504
  target.send = function (data) {
494
- var bytes = typeof data === 'string'
495
- ? new TextEncoder().encode(data)
496
- : new Uint8Array(data)
505
+ var isBinary = typeof data !== 'string'
506
+ var bytes = isBinary ? new Uint8Array(data) : new TextEncoder().encode(data)
497
507
  var sealed = sealEnvelope(bytes)
498
508
  var bin = ''
499
509
  for (var i = 0; i < sealed.length; i++) bin += String.fromCharCode(sealed[i])
500
- origSend(JSON.stringify({ __relay: btoa(bin) }))
510
+ var wrapped = { __relay: btoa(bin) }
511
+ if (isBinary) wrapped.__bin = 1
512
+ origSend(JSON.stringify(wrapped))
501
513
  }
502
514
  Object.defineProperty(target, 'onmessage', {
503
515
  get: function () { return userListener },
package/src/pump.js CHANGED
@@ -1100,10 +1100,12 @@ export class PumpManager {
1100
1100
  let out
1101
1101
  if (isDataFrame(frame.opcode)) {
1102
1102
  let plain = null
1103
+ let binary = false
1103
1104
  try {
1104
1105
  const json = JSON.parse(frame.payload.toString('utf8'))
1105
1106
  if (json && typeof json.__relay === 'string') {
1106
1107
  plain = this.cryptoBox.open(this.cryptoBox.envelopeKey, Buffer.from(json.__relay, 'base64'))
1108
+ binary = json.__bin === 1
1107
1109
  }
1108
1110
  } catch {}
1109
1111
  if (!plain) {
@@ -1117,10 +1119,10 @@ export class PumpManager {
1117
1119
  }
1118
1120
  plain = frame.payload
1119
1121
  }
1120
- // DSH 0.2's API gateway closes a binary frame with 1003 ("text
1121
- // messages required"). The boot patch always sends JSON text, so
1122
- // re-frame the decrypted payload as text upstream.
1123
- out = buildFrame(OPCODES.OP_TEXT, plain, { mask: true })
1122
+ // Text stays text (the JSON protocol requires it) and binary stays binary:
1123
+ // an ArrayBuffer payload forwarded as text is rejected by the host, and a
1124
+ // text payload forwarded as binary is invisible to it.
1125
+ out = buildFrame(binary ? OPCODES.OP_BINARY : OPCODES.OP_TEXT, plain, { mask: true })
1124
1126
  } else {
1125
1127
  out = buildFrame(frame.opcode, frame.payload, { mask: true, fin: frame.fin })
1126
1128
  }
@@ -1149,7 +1151,13 @@ export class PumpManager {
1149
1151
  : frame.payload
1150
1152
  stream.wsFragment = null
1151
1153
  const sealed = this.cryptoBox.seal(this.cryptoBox.envelopeKey, payload)
1152
- const json = Buffer.from(JSON.stringify({ __relay: sealed.toString('base64') }))
1154
+ // Carry the message kind across the seal. The client sets
1155
+ // binaryType="arraybuffer" and the account avatar arrives as binary;
1156
+ // decoding it as text corrupts the bytes and the app then breaks the
1157
+ // connection.
1158
+ const wrapped = { __relay: sealed.toString('base64') }
1159
+ if (frame.opcode === OPCODES.OP_BINARY) wrapped.__bin = 1
1160
+ const json = Buffer.from(JSON.stringify(wrapped))
1153
1161
  this.client?.sendStream(sid, buildFrame(OPCODES.OP_TEXT, json, { mask: false }))
1154
1162
  continue
1155
1163
  }
@@ -167,13 +167,13 @@ export class RelayClient extends EventTarget {
167
167
  let msg
168
168
  try { msg = JSON.parse(data.toString()) } catch { return }
169
169
  if (msg.t === 'challenge') {
170
- // The relay stores only sha256(key) for a tenant key, so a tenant proof
171
- // is keyed by that hash; the legacy admin --key is keyed by the key
172
- // itself. Older relays do not send `mode` and only ever meant the admin
173
- // derivation, so that stays the fallback.
170
+ // A tenant proof is keyed by a hash of the relay key (the relay never
171
+ // stores the key itself); the legacy admin --key is keyed by the key.
172
+ // Version 2 domain-separates the secret from the key id, and an older
173
+ // relay sends no `v`, so the v1 derivation stays the fallback.
174
174
  const key = String(this.relayKey)
175
175
  const secret = msg.mode === 'tenant'
176
- ? crypto.createHash('sha256').update(key).digest('hex')
176
+ ? crypto.createHash('sha256').update(msg.v >= 2 ? `dsh-relay:hmac:v2:${key}` : key).digest('hex')
177
177
  : key
178
178
  const proof = crypto.createHmac('sha256', secret)
179
179
  .update(`${msg.nonce}|${this.identity.id}`).digest('hex')