dsh-relay-phone 0.11.1 → 0.11.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-relay-phone",
3
- "version": "0.11.1",
3
+ "version": "0.11.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/panel.js CHANGED
@@ -89,9 +89,17 @@ export async function registerPanel(ctx, service) {
89
89
  kind: 'exact',
90
90
  path: '/relay-bridge/panel',
91
91
  handler: async (req, res) => {
92
- // Same ticket policy as /relay-bridge/api: relayed requests already
93
- // carry a ticket; a local page load mints one so its write actions
94
- // authenticate.
92
+ // Desktop-only. This page hands out the pairing entry URL, which embeds
93
+ // the device PSK (`#k=`); a relayed request is a phone, and a phone that
94
+ // could read it would be able to pair unlimited extra devices. The pump
95
+ // marks relayed requests, so this cannot be forged by the client.
96
+ if (req.headers['x-dsh-relay']) {
97
+ res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' })
98
+ res.end('dsh-relay: the management panel is desktop-only. Open it on 127.0.0.1.\n')
99
+ return
100
+ }
101
+ // Same ticket policy as /relay-bridge/api: a local page load mints one so
102
+ // its write actions authenticate.
95
103
  if (!service.pairing.sessionForHeaders(req.headers)) {
96
104
  if (!isLoopback(req.socket?.remoteAddress ?? '')) {
97
105
  res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' })
@@ -117,6 +125,13 @@ export async function registerPanel(ctx, service) {
117
125
  res.end(JSON.stringify(value))
118
126
  }
119
127
 
128
+ // Desktop-only, for the same reason as the page: the snapshot contains
129
+ // the pairing URL with the device PSK, and the relay key can be changed
130
+ // from here.
131
+ if (req.headers['x-dsh-relay']) {
132
+ return reply(403, { ok: false, error: 'desktop-only' })
133
+ }
134
+
120
135
  // Pairing-ticket gate. Requests arriving through the relay already
121
136
  // carry a valid ticket (the pump refuses ticketless traffic), so this
122
137
  // check only ever fires for DIRECT local access — the local user's own
package/src/pump.js CHANGED
@@ -365,6 +365,17 @@ export class PumpManager {
365
365
  return
366
366
  }
367
367
 
368
+ // The plugin's own console is a desktop tool: it prints the pairing URL
369
+ // (which carries the device PSK) and can change the relay key. A phone that
370
+ // reached it would be able to pair unlimited extra devices, so relayed
371
+ // requests never see it, ticketed or not.
372
+ if (stream.relay && info.path.startsWith('/relay-bridge/')) {
373
+ auditLog(`relayed request to the management panel blocked: ${info.path}`)
374
+ this.respondInline(sid, 403, 'Forbidden', 'dsh-relay-phone: the management panel is desktop-only\n')
375
+ this.closeStream(sid)
376
+ return
377
+ }
378
+
368
379
  if (!session) {
369
380
  auditLog(`unauthenticated ${info.method} ${info.path} -> pairing page`)
370
381
  this.respond(sid, 200, 'text/html; charset=utf-8',
@@ -530,10 +541,12 @@ export class PumpManager {
530
541
  if (lower.startsWith('connection:')) continue
531
542
  if (lower.startsWith('accept-encoding:')) continue
532
543
  if (lower.startsWith('x-relay-enc:') || lower.startsWith('x-relay-ct:')) continue
544
+ if (lower.startsWith('x-dsh-relay:')) continue // the pump sets its own
533
545
  headers.push(line)
534
546
  }
535
547
  headers.unshift(`Host: ${target.authority}`)
536
548
  headers.unshift(`Origin: ${target.origin}`)
549
+ headers.push('x-dsh-relay: 1')
537
550
  if (authCookie) headers.push(`Cookie: ${authCookie}`)
538
551
  headers.push(`Content-Type: ${info.headers.get('x-relay-ct') ?? 'text/plain;charset=UTF-8'}`)
539
552
  headers.push(`Content-Length: ${plain.length}`)
@@ -603,6 +616,8 @@ export class PumpManager {
603
616
  hasHost = true
604
617
  } else if (/^origin:/i.test(line)) {
605
618
  rewritten.push(`Origin: ${target.origin}`)
619
+ } else if (/^x-dsh-relay:/i.test(line)) {
620
+ // Dropped: the pump adds its own marker below.
606
621
  } else if (/^cookie:/i.test(line)) {
607
622
  // The phone's relay ticket authenticates it to the pump, not to DSH;
608
623
  // upstream speaks the browser-session credential instead.
@@ -621,6 +636,10 @@ export class PumpManager {
621
636
  }
622
637
  if (!hasHost) rewritten.unshift(`Host: ${target.authority}`)
623
638
  if (!isUpgrade && !hasConnection) rewritten.push('Connection: close')
639
+ // Mark the request as relayed. The plugin's own console uses this to refuse
640
+ // phone-side access: it hands out pairing links, which embed the device PSK.
641
+ // Any inbound copy was dropped in the loop above, so a client cannot forge it.
642
+ rewritten.push('x-dsh-relay: 1')
624
643
  if (authCookie) rewritten.push(`Cookie: ${authCookie}`)
625
644
  const out = `${lines[0]}\r\n${rewritten.join('\r\n')}\r\n\r\n`
626
645