From cbe7663835840834c05758b89c5fc6b683a3ad09 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 4 May 2026 18:49:43 -0700 Subject: [PATCH 1/2] fix(relay): carry binary tunnel WS frames as base64 The host-service tunnel client UTF-8-decoded binary WS frames from its local loopback socket via String(buffer), corrupting PTY output bytes (901622573 made PTY output binary end-to-end). The relay then forwarded the mangled string as a text frame, and the renderer's JSON.parse fell through to "[terminal] invalid server payload". Add an optional encoding: "base64" discriminator to TunnelWsFrame. tunnel-client base64-encodes ArrayBuffer frames; relay decodes and emits a real binary WS frame to the browser. Renderer's existing ArrayBuffer fast path handles it unchanged. Deploy relay first, then desktop canary picks up the host-service tunnel-client change. --- apps/relay/src/tunnel.ts | 10 ++++++++-- packages/host-service/src/tunnel/tunnel-client.ts | 15 ++++++++++++++- packages/shared/src/tunnel-protocol.ts | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/relay/src/tunnel.ts b/apps/relay/src/tunnel.ts index 9eeaac1ce00..c245ce60854 100644 --- a/apps/relay/src/tunnel.ts +++ b/apps/relay/src/tunnel.ts @@ -2,7 +2,7 @@ import { createApiClient } from "./api-client"; import type { TunnelHttpResponse, TunnelRequest } from "./types"; type WsSocket = { - send: (data: string) => void; + send: (data: string | ArrayBuffer | Uint8Array) => void; readyState: number; close: (code?: number, reason?: string) => void; }; @@ -174,7 +174,13 @@ export class TunnelManager { } } else if (msg.type === "ws:frame") { const clientWs = tunnel.activeChannels.get(msg.id as string); - if (clientWs?.readyState === 1) clientWs.send(msg.data as string); + if (clientWs?.readyState === 1) { + if (msg.encoding === "base64") { + clientWs.send(Buffer.from(msg.data as string, "base64")); + } else { + clientWs.send(msg.data as string); + } + } } else if (msg.type === "ws:close") { const clientWs = tunnel.activeChannels.get(msg.id as string); if (clientWs) { diff --git a/packages/host-service/src/tunnel/tunnel-client.ts b/packages/host-service/src/tunnel/tunnel-client.ts index 42d0e46771e..20a0268239e 100644 --- a/packages/host-service/src/tunnel/tunnel-client.ts +++ b/packages/host-service/src/tunnel/tunnel-client.ts @@ -191,9 +191,22 @@ export class TunnelClient { } const localWs = new WebSocket(wsUrl.toString()); + localWs.binaryType = "arraybuffer"; localWs.onmessage = (event) => { - this.send({ type: "ws:frame", id: request.id, data: String(event.data) }); + const data = event.data; + if (typeof data === "string") { + this.send({ type: "ws:frame", id: request.id, data }); + return; + } + if (data instanceof ArrayBuffer) { + this.send({ + type: "ws:frame", + id: request.id, + data: Buffer.from(data).toString("base64"), + encoding: "base64", + }); + } }; localWs.onclose = (event) => { diff --git a/packages/shared/src/tunnel-protocol.ts b/packages/shared/src/tunnel-protocol.ts index bb199865089..7bafd07dba7 100644 --- a/packages/shared/src/tunnel-protocol.ts +++ b/packages/shared/src/tunnel-protocol.ts @@ -20,6 +20,7 @@ export interface TunnelWsFrame { type: "ws:frame"; id: string; data: string; + encoding?: "base64"; } export interface TunnelWsClose { From ad1445ab87a37f197da0090cb55d10078cb6f2e2 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 4 May 2026 18:56:29 -0700 Subject: [PATCH 2/2] fix(relay): guard ws:frame data type before decode handleMessage runs uncaught; a malformed frame with non-string data would throw at Buffer.from and could tear down the host's tunnel connection. Drop frames whose data isn't a string. --- apps/relay/src/tunnel.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/relay/src/tunnel.ts b/apps/relay/src/tunnel.ts index c245ce60854..bf112e63d96 100644 --- a/apps/relay/src/tunnel.ts +++ b/apps/relay/src/tunnel.ts @@ -173,12 +173,13 @@ export class TunnelManager { pending.resolve(msg as unknown as TunnelHttpResponse); } } else if (msg.type === "ws:frame") { + if (typeof msg.data !== "string") return; const clientWs = tunnel.activeChannels.get(msg.id as string); if (clientWs?.readyState === 1) { if (msg.encoding === "base64") { - clientWs.send(Buffer.from(msg.data as string, "base64")); + clientWs.send(Buffer.from(msg.data, "base64")); } else { - clientWs.send(msg.data as string); + clientWs.send(msg.data); } } } else if (msg.type === "ws:close") {