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:
co-authored by
Claude
Mohamed Boudra
parent
098122be8d
commit
73ba37a07a
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface PaseoClientConfig {
|
||||
runtimeGeneration?: number | null;
|
||||
password?: string;
|
||||
authHeader?: string;
|
||||
headers?: Record<string, string>;
|
||||
suppressSendErrors?: boolean;
|
||||
logger?: PaseoLogger;
|
||||
connectTimeoutMs?: number;
|
||||
|
||||
Reference in New Issue
Block a user