Add custom headers to direct connections (#2922)

* Add custom headers to direct connections

* Address custom header review feedback

* Fix direct connection header integration

* Fix desktop relay E2E transport setup

---------

Co-authored-by: Claude <claude@Claudes-Mac-mini.local>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Claude
2026-08-06 11:05:40 +02:00
committed by GitHub
co-authored by Claude Mohamed Boudra
parent 098122be8d
commit 73ba37a07a
38 changed files with 1190 additions and 125 deletions
@@ -9,6 +9,7 @@ import {
encodeUtf8String,
extractRelayMessage,
} from "./daemon-client-transport.js";
import { defaultWebSocketFactory } from "./daemon-client-websocket-transport.js";
const createClientChannelMock = vi.hoisted(() => vi.fn());
@@ -17,6 +18,28 @@ vi.mock("@getpaseo/relay/e2ee", () => ({
}));
describe("daemon-client transport helpers", () => {
test("defaultWebSocketFactory passes custom headers to native WebSocket options", () => {
const constructorSpy = vi.fn();
function MockWebSocket(
url: string,
protocols?: string | string[],
options?: { headers?: Record<string, string> },
) {
constructorSpy(url, protocols, options);
}
vi.stubGlobal("WebSocket", MockWebSocket);
defaultWebSocketFactory("ws://example.test", {
protocols: ["paseo.test"],
headers: { "X-Tenant": "acme" },
});
expect(constructorSpy).toHaveBeenCalledWith("ws://example.test", ["paseo.test"], {
headers: { "X-Tenant": "acme" },
});
vi.unstubAllGlobals();
});
test("createEncryptedTransport closes handshake failures with browser-safe code", async () => {
createClientChannelMock.mockReset();
createClientChannelMock.mockRejectedValueOnce(new Error("handshake failed"));
@@ -11,13 +11,17 @@ export function defaultWebSocketFactory(
): WebSocketLike {
const globalWs = (
globalThis as {
WebSocket?: new (url: string, protocols?: string | string[]) => WebSocketLike;
WebSocket?: new (
url: string,
protocols?: string | string[],
options?: { headers?: Record<string, string> },
) => WebSocketLike;
}
).WebSocket;
if (!globalWs) {
throw new Error("WebSocket is not available in this runtime");
}
return new globalWs(url, options?.protocols);
return new globalWs(url, options?.protocols, { headers: options?.headers });
}
export function createWebSocketTransportFactory(factory: WebSocketFactory): DaemonTransportFactory {
+27
View File
@@ -707,6 +707,33 @@ test("passes password as HTTP bearer header and WebSocket subprotocol", async ()
});
});
test("passes custom headers and keeps password authorization authoritative", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const transportFactory = vi.fn(() => mock.transport);
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
password: "shared-secret",
headers: { "X-Tenant": "acme", Authorization: "Bearer ignored" },
logger,
reconnect: { enabled: false },
transportFactory,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
expect(transportFactory).toHaveBeenCalledWith({
url: "ws://test",
headers: { "X-Tenant": "acme", Authorization: "Bearer shared-secret" },
protocols: ["paseo.bearer.shared-secret"],
});
});
test("advertises client capabilities in hello", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
+2 -1
View File
@@ -301,6 +301,7 @@ export interface DaemonClientConfig {
runtimeGeneration?: number | null;
password?: string;
authHeader?: string;
headers?: Record<string, string>;
suppressSendErrors?: boolean;
transportFactory?: DaemonTransportFactory;
webSocketFactory?: WebSocketFactory;
@@ -1249,7 +1250,7 @@ export class DaemonClient {
return;
}
const headers: Record<string, string> = {};
const headers: Record<string, string> = { ...this.config.headers };
const password = normalizePassword(this.config.password);
if (password) {
headers.Authorization = `Bearer ${password}`;
+1
View File
@@ -57,6 +57,7 @@ export interface PaseoClientConfig {
runtimeGeneration?: number | null;
password?: string;
authHeader?: string;
headers?: Record<string, string>;
suppressSendErrors?: boolean;
logger?: PaseoLogger;
connectTimeoutMs?: number;