Keep daemon credentials out of relay upgrades without E2EE

Restores the relay upgrade boundary removed in the boundary extraction: a
credentialed DaemonClient on a relay URL with E2EE omitted sent authHeader to
the relay. Regression test through the public DaemonClient API.
This commit is contained in:
Mohamed Boudra
2026-10-21 13:39:31 +02:00
parent 50cde73137
commit da7f84879c
4 changed files with 39 additions and 1 deletions
@@ -37,6 +37,16 @@ export function createRelayE2eeTransportFactory(args: {
};
}
/**
* Relay upgrades carry routing only. Daemon credentials and subprotocols must
* never reach the relay; they travel inside the encrypted hello.
*/
export function createRelayUpgradeTransportFactory(
baseFactory: DaemonTransportFactory,
): DaemonTransportFactory {
return ({ url }) => baseFactory({ url });
}
export function createEncryptedTransport(
base: DaemonTransport,
daemonPublicKeyB64: string,
@@ -17,6 +17,7 @@ export {
export {
createEncryptedTransport,
createRelayE2eeTransportFactory,
createRelayUpgradeTransportFactory,
} from "./daemon-client-relay-e2ee-transport.js";
export {
bindWsHandler,
+23
View File
@@ -919,6 +919,29 @@ test("keeps relay upgrade credentials out of the socket request", async () => {
await expect(connecting).rejects.toThrow("Daemon client closed");
});
test("keeps relay upgrade credentials out of the socket request without E2EE", async () => {
const mock = createMockTransport();
const requests: Array<{ url: string; headers?: Record<string, string>; protocols?: string[] }> =
[];
const client = new DaemonClient({
url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2",
clientId: "clsk_relay_header_test",
authHeader: "Bearer synthetic-review-token",
reconnect: { enabled: false },
transportFactory: (request) => {
requests.push(request);
return mock.transport;
},
});
clients.push(client);
const connecting = client.connect();
void connecting.catch(() => {});
await vi.waitFor(() => expect(requests).toHaveLength(1));
expect(requests[0]).toEqual({ url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2" });
await client.close();
await expect(connecting).rejects.toThrow("Daemon client closed");
});
test("refuses relay password auth without an encrypted hello", async () => {
const requests: unknown[] = [];
const client = new DaemonClient({
+5 -1
View File
@@ -162,6 +162,7 @@ import {
} from "@getpaseo/protocol/binary-frames/index";
import {
createRelayE2eeTransportFactory,
createRelayUpgradeTransportFactory,
createWebSocketTransportFactory,
decodeMessageData,
defaultWebSocketFactory,
@@ -1449,8 +1450,11 @@ export class DaemonClient {
const isRelayTransport = isRelayClientWebSocketUrl(this.config.url);
this.assertEncryptedRelayAuth(selected.helloAuth, isRelayTransport);
// Relay upgrades are routing only: credentials never reach the relay socket.
let transportFactory = isRelayTransport
? createRelayUpgradeTransportFactory(baseTransportFactory)
: baseTransportFactory;
// Endpoint security is selected by configuration, never inferred from the URL.
let transportFactory = baseTransportFactory;
if (this.config.e2ee?.enabled === true) {
const daemonPublicKeyB64 = this.config.e2ee.daemonPublicKeyB64;
if (!daemonPublicKeyB64) {