relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
284 lines (266 loc) • 10.5 kB
JavaScript
import net from "node:net";
import tls from "node:tls";
export const E2EE_TUNNEL_FRAME_VERSION = 1;
export const E2EE_TUNNEL_FRAME_HEADER_BYTES = 10;
export const E2EE_TUNNEL_MAX_FRAME_BYTES = 64 * 1024;
export const E2EE_TUNNEL_FRAME = Object.freeze({
register: 1,
registered: 2,
open: 3,
data: 4,
close: 5,
ping: 6,
pong: 7,
error: 8,
});
export function encodeE2eeTunnelFrame(type, streamId = 0, payload = Buffer.alloc(0)) {
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
if (!Number.isInteger(type) || type < 1 || type > 255) throw new Error("Invalid E2EE tunnel frame type");
if (!Number.isInteger(streamId) || streamId < 0 || streamId > 0xffffffff) throw new Error("Invalid E2EE tunnel stream id");
if (body.length > E2EE_TUNNEL_MAX_FRAME_BYTES) throw new Error("E2EE tunnel frame is too large");
const header = Buffer.allocUnsafe(E2EE_TUNNEL_FRAME_HEADER_BYTES);
header.writeUInt8(E2EE_TUNNEL_FRAME_VERSION, 0);
header.writeUInt8(type, 1);
header.writeUInt32BE(streamId, 2);
header.writeUInt32BE(body.length, 6);
return body.length ? Buffer.concat([header, body]) : header;
}
export class E2eeTunnelFrameDecoder {
constructor() {
this.buffered = Buffer.alloc(0);
}
push(chunk) {
if (!Buffer.isBuffer(chunk)) throw new Error("E2EE tunnel input must be bytes");
this.buffered = this.buffered.length ? Buffer.concat([this.buffered, chunk]) : Buffer.from(chunk);
const frames = [];
while (this.buffered.length >= E2EE_TUNNEL_FRAME_HEADER_BYTES) {
if (this.buffered.readUInt8(0) !== E2EE_TUNNEL_FRAME_VERSION) throw new Error("Unsupported E2EE tunnel frame version");
const length = this.buffered.readUInt32BE(6);
if (length > E2EE_TUNNEL_MAX_FRAME_BYTES) throw new Error("E2EE tunnel frame is too large");
const total = E2EE_TUNNEL_FRAME_HEADER_BYTES + length;
if (this.buffered.length < total) break;
frames.push({
type: this.buffered.readUInt8(1),
streamId: this.buffered.readUInt32BE(2),
payload: this.buffered.subarray(E2EE_TUNNEL_FRAME_HEADER_BYTES, total),
});
this.buffered = this.buffered.subarray(total);
}
if (this.buffered.length > E2EE_TUNNEL_MAX_FRAME_BYTES + E2EE_TUNNEL_FRAME_HEADER_BYTES) {
throw new Error("E2EE tunnel frame buffer exceeded its limit");
}
return frames;
}
}
function frameChunks(type, streamId, payload) {
const frames = [];
for (let offset = 0; offset < payload.length; offset += E2EE_TUNNEL_MAX_FRAME_BYTES) {
frames.push(encodeE2eeTunnelFrame(type, streamId, payload.subarray(offset, offset + E2EE_TUNNEL_MAX_FRAME_BYTES)));
}
return frames;
}
function defaultGatewayConnect({ host, port, servername }) {
return tls.connect({ host, port, servername, minVersion: "TLSv1.3", rejectUnauthorized: true });
}
function defaultLocalConnect({ host, port }) {
return net.connect({ host, port });
}
/**
* Multiplex opaque public TLS connections from the blind gateway into the
* device-owned HTTPS origin. DATA frames are never parsed or logged here.
*/
export class E2eeMcpTunnelClient {
constructor({
gatewayHost,
gatewayPort = 8443,
gatewayServerName = gatewayHost,
localHost = "127.0.0.1",
localPort,
leaseProvider,
connectGateway = defaultGatewayConnect,
connectLocal = defaultLocalConnect,
reconnect = true,
reconnectMinMs = 1_000,
reconnectMaxMs = 30_000,
pingIntervalMs = 25_000,
} = {}) {
if (!gatewayHost) throw new Error("E2EE tunnel gateway host is required");
if (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("E2EE tunnel local port is required");
if (typeof leaseProvider !== "function") throw new Error("E2EE tunnel lease provider is required");
this.options = {
gatewayHost,
gatewayPort,
gatewayServerName,
localHost,
localPort,
leaseProvider,
connectGateway,
connectLocal,
reconnect,
reconnectMinMs,
reconnectMaxMs,
pingIntervalMs,
};
this.gateway = null;
this.decoder = null;
this.streams = new Map();
this.stopped = true;
this.connecting = null;
this.reconnectDelayMs = reconnectMinMs;
this.reconnectTimer = null;
this.pingTimer = null;
}
start() {
if (!this.stopped) return this.connecting || Promise.resolve();
this.stopped = false;
this.connecting = this.#connect();
return this.connecting;
}
async stop() {
this.stopped = true;
clearTimeout(this.reconnectTimer);
clearInterval(this.pingTimer);
this.reconnectTimer = null;
this.pingTimer = null;
const gateway = this.gateway;
this.gateway = null;
for (const stream of this.streams.values()) stream.destroy();
this.streams.clear();
if (gateway && !gateway.destroyed) {
await new Promise((resolve) => {
gateway.once("close", resolve);
gateway.destroy();
});
}
}
#write(type, streamId, payload = Buffer.alloc(0)) {
if (!this.gateway || this.gateway.destroyed) return false;
return this.gateway.write(encodeE2eeTunnelFrame(type, streamId, payload));
}
#closeStream(streamId, { notify = true } = {}) {
const stream = this.streams.get(streamId);
if (!stream) return;
this.streams.delete(streamId);
if (notify) this.#write(E2EE_TUNNEL_FRAME.close, streamId);
if (!stream.destroyed) stream.destroy();
}
#openLocal(streamId) {
if (!streamId || this.streams.has(streamId)) throw new Error("Invalid duplicate E2EE tunnel stream");
const stream = this.options.connectLocal({ host: this.options.localHost, port: this.options.localPort });
this.streams.set(streamId, stream);
stream.setNoDelay?.(true);
stream.on("data", (chunk) => {
if (!this.gateway || this.gateway.destroyed) return stream.destroy();
let writable = true;
for (const frame of frameChunks(E2EE_TUNNEL_FRAME.data, streamId, chunk)) {
writable = this.gateway.write(frame) && writable;
}
if (!writable) stream.pause();
});
stream.on("drain", () => this.gateway?.resume());
stream.on("close", () => {
if (this.streams.get(streamId) !== stream) return;
this.streams.delete(streamId);
this.#write(E2EE_TUNNEL_FRAME.close, streamId);
});
stream.on("error", () => this.#closeStream(streamId));
}
#handleFrame(frame, registered) {
if (!registered.done) {
if (frame.type === E2EE_TUNNEL_FRAME.error) throw new Error(frame.payload.toString("utf8") || "E2EE tunnel registration failed");
if (frame.type !== E2EE_TUNNEL_FRAME.registered || frame.streamId !== 0) throw new Error("E2EE tunnel gateway did not accept registration");
registered.done = true;
this.reconnectDelayMs = this.options.reconnectMinMs;
registered.resolve();
return;
}
if (frame.type === E2EE_TUNNEL_FRAME.pong && frame.streamId === 0) return;
if (frame.type === E2EE_TUNNEL_FRAME.open) return this.#openLocal(frame.streamId);
const stream = this.streams.get(frame.streamId);
if (!stream) throw new Error("E2EE tunnel gateway referenced an unknown stream");
if (frame.type === E2EE_TUNNEL_FRAME.data) {
if (!stream.write(frame.payload)) this.gateway?.pause();
return;
}
if (frame.type === E2EE_TUNNEL_FRAME.close) return this.#closeStream(frame.streamId, { notify: false });
if (frame.type === E2EE_TUNNEL_FRAME.error) throw new Error(frame.payload.toString("utf8") || "E2EE tunnel gateway error");
throw new Error("Invalid E2EE tunnel gateway frame");
}
async #connect() {
const registration = await this.options.leaseProvider();
if (this.stopped) return;
const endpointId = String(registration?.endpointId || "").toLowerCase();
const lease = String(registration?.lease || "");
if (!endpointId || !lease) throw new Error("E2EE tunnel lease provider returned no endpoint or lease");
this.decoder = new E2eeTunnelFrameDecoder();
const gateway = this.options.connectGateway({
host: this.options.gatewayHost,
port: this.options.gatewayPort,
servername: this.options.gatewayServerName,
});
this.gateway = gateway;
gateway.setNoDelay?.(true);
gateway.setKeepAlive?.(true, 30_000);
let settled = false;
let resolveReady;
let rejectReady;
const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
const registered = {
done: false,
resolve: () => { if (!settled) { settled = true; resolveReady(); } },
};
const fail = (error) => {
if (!settled) {
settled = true;
rejectReady(error instanceof Error ? error : new Error(String(error)));
}
gateway.destroy();
};
gateway.once(gateway.encrypted ? "secureConnect" : "connect", () => {
gateway.write(encodeE2eeTunnelFrame(
E2EE_TUNNEL_FRAME.register,
0,
JSON.stringify({ endpointId, lease }),
));
});
gateway.on("data", (chunk) => {
try {
for (const frame of this.decoder.push(chunk)) this.#handleFrame(frame, registered);
} catch (error) {
fail(error);
}
});
gateway.on("drain", () => {
for (const stream of this.streams.values()) stream.resume();
});
gateway.once("error", fail);
gateway.once("close", () => {
clearInterval(this.pingTimer);
this.pingTimer = null;
if (this.gateway === gateway) this.gateway = null;
for (const stream of this.streams.values()) stream.destroy();
this.streams.clear();
if (!settled) {
settled = true;
rejectReady(new Error("E2EE tunnel closed before registration completed"));
}
if (!this.stopped && this.options.reconnect) {
const delay = this.reconnectDelayMs;
this.reconnectDelayMs = Math.min(this.options.reconnectMaxMs, Math.max(delay + 1, delay * 2));
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connecting = this.#connect().catch(() => {});
}, delay);
this.reconnectTimer.unref?.();
}
});
await ready;
if (!this.stopped) {
clearInterval(this.pingTimer);
this.pingTimer = setInterval(() => {
this.#write(E2EE_TUNNEL_FRAME.ping, 0, Buffer.from(String(Date.now())));
}, this.options.pingIntervalMs);
this.pingTimer.unref?.();
}
}
}