refactor: centralize relay vs direct E2EE
This commit is contained in:
@@ -22,11 +22,11 @@ function buildCandidateUrls(daemon: HostProfile): string[] {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isLastKnownRelay = !!relayEndpoint && lastKnownGood === relayEndpoint;
|
const isLastKnownRelay = !!relayEndpoint && lastKnownGood === relayEndpoint;
|
||||||
const directEndpoints = relayEndpoint
|
const directEndpoints = endpoints;
|
||||||
? endpoints.filter((endpoint) => endpoint !== relayEndpoint)
|
|
||||||
: endpoints;
|
|
||||||
|
|
||||||
if (lastKnownGood && !isLastKnownRelay) {
|
if (sessionId && relayEndpoint && isLastKnownRelay) {
|
||||||
|
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||||
|
} else if (lastKnownGood) {
|
||||||
push(buildDaemonWebSocketUrl(lastKnownGood));
|
push(buildDaemonWebSocketUrl(lastKnownGood));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +35,6 @@ function buildCandidateUrls(daemon: HostProfile): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId && relayEndpoint) {
|
if (sessionId && relayEndpoint) {
|
||||||
if (isLastKnownRelay) {
|
|
||||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
|
||||||
}
|
|
||||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { createContext, useCallback, useContext } from "react";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
import {
|
||||||
buildDaemonWebSocketUrl,
|
buildDaemonWebSocketUrl,
|
||||||
decodeOfferFragmentPayload,
|
decodeOfferFragmentPayload,
|
||||||
@@ -10,6 +9,10 @@ import {
|
|||||||
extractHostPortFromWebSocketUrl,
|
extractHostPortFromWebSocketUrl,
|
||||||
normalizeHostPort,
|
normalizeHostPort,
|
||||||
} from "@/utils/daemon-endpoints";
|
} from "@/utils/daemon-endpoints";
|
||||||
|
import {
|
||||||
|
ConnectionOfferV1Schema,
|
||||||
|
type ConnectionOfferV1,
|
||||||
|
} from "@server/shared/connection-offer";
|
||||||
|
|
||||||
const REGISTRY_STORAGE_KEY = "@paseo:daemon-registry";
|
const REGISTRY_STORAGE_KEY = "@paseo:daemon-registry";
|
||||||
const LEGACY_SETTINGS_KEY = "@paseo:settings";
|
const LEGACY_SETTINGS_KEY = "@paseo:settings";
|
||||||
@@ -41,15 +44,6 @@ type CreateHostInput = {
|
|||||||
|
|
||||||
type UpdateHostInput = Partial<Omit<HostProfile, "id" | "createdAt">>;
|
type UpdateHostInput = Partial<Omit<HostProfile, "id" | "createdAt">>;
|
||||||
|
|
||||||
const ConnectionOfferV1Schema = z.object({
|
|
||||||
v: z.literal(1),
|
|
||||||
sessionId: z.string().min(1),
|
|
||||||
endpoints: z.array(z.string().min(1)).min(1),
|
|
||||||
daemonPublicKeyB64: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ConnectionOfferV1 = z.infer<typeof ConnectionOfferV1Schema>;
|
|
||||||
|
|
||||||
interface DaemonRegistryContextValue {
|
interface DaemonRegistryContextValue {
|
||||||
daemons: HostProfile[];
|
daemons: HostProfile[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -134,7 +128,12 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
const existing = readDaemons();
|
const existing = readDaemons();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const normalizedEndpoints = offer.endpoints.map((endpoint) => normalizeHostPort(endpoint));
|
const normalizedEndpoints = offer.endpoints.map((endpoint) => normalizeHostPort(endpoint));
|
||||||
const relayEndpoint = normalizedEndpoints[normalizedEndpoints.length - 1];
|
const relayEndpoint =
|
||||||
|
offer.relay?.endpoint
|
||||||
|
? normalizeHostPort(offer.relay.endpoint)
|
||||||
|
: offer.relay === undefined && normalizedEndpoints.length > 0
|
||||||
|
? normalizedEndpoints[normalizedEndpoints.length - 1]
|
||||||
|
: null;
|
||||||
|
|
||||||
const matchIndex = existing.findIndex((daemon) => daemon.daemonPublicKeyB64 === offer.daemonPublicKeyB64);
|
const matchIndex = existing.findIndex((daemon) => daemon.daemonPublicKeyB64 === offer.daemonPublicKeyB64);
|
||||||
if (matchIndex !== -1) {
|
if (matchIndex !== -1) {
|
||||||
@@ -142,7 +141,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
...existing[matchIndex],
|
...existing[matchIndex],
|
||||||
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
||||||
endpoints: normalizedEndpoints,
|
endpoints: normalizedEndpoints,
|
||||||
relay: { endpoint: relayEndpoint, sessionId: offer.sessionId },
|
relay: relayEndpoint ? { endpoint: relayEndpoint, sessionId: offer.sessionId } : null,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
const next = [...existing];
|
const next = [...existing];
|
||||||
@@ -156,7 +155,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
label: deriveLabelFromEndpoint(normalizedEndpoints[0] ?? "Unnamed Host"),
|
label: deriveLabelFromEndpoint(normalizedEndpoints[0] ?? "Unnamed Host"),
|
||||||
endpoints: normalizedEndpoints,
|
endpoints: normalizedEndpoints,
|
||||||
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
||||||
relay: { endpoint: relayEndpoint, sessionId: offer.sessionId },
|
relay: relayEndpoint ? { endpoint: relayEndpoint, sessionId: offer.sessionId } : null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
|
|||||||
@@ -13,15 +13,6 @@ type DaemonClientOptions = {
|
|||||||
daemonPublicKeyB64?: string;
|
daemonPublicKeyB64?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isRelayWebSocketUrl(url: string): boolean {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url);
|
|
||||||
return parsed.searchParams.get("role") === "client" && parsed.searchParams.has("session");
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDaemonClient(
|
export function useDaemonClient(
|
||||||
url: string,
|
url: string,
|
||||||
options: DaemonClientOptions = {}
|
options: DaemonClientOptions = {}
|
||||||
@@ -29,21 +20,18 @@ export function useDaemonClient(
|
|||||||
const client = useMemo(
|
const client = useMemo(
|
||||||
() => {
|
() => {
|
||||||
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
||||||
const relayConnection = isRelayWebSocketUrl(url);
|
|
||||||
return new DaemonClientV2({
|
return new DaemonClientV2({
|
||||||
url,
|
url,
|
||||||
suppressSendErrors: true,
|
suppressSendErrors: true,
|
||||||
...(tauriTransportFactory
|
...(tauriTransportFactory
|
||||||
? { transportFactory: tauriTransportFactory }
|
? { transportFactory: tauriTransportFactory }
|
||||||
: {}),
|
: {}),
|
||||||
...(relayConnection
|
e2ee: options.daemonPublicKeyB64
|
||||||
? {
|
? {
|
||||||
e2ee: {
|
enabled: true,
|
||||||
enabled: true,
|
daemonPublicKeyB64: options.daemonPublicKeyB64,
|
||||||
daemonPublicKeyB64: options.daemonPublicKeyB64,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: {}),
|
: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[options.daemonPublicKeyB64, url]
|
[options.daemonPublicKeyB64, url]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
|
"./e2ee": "./src/e2ee.ts",
|
||||||
"./node": "./src/node-adapter.ts",
|
"./node": "./src/node-adapter.ts",
|
||||||
"./cloudflare": "./src/cloudflare-adapter.ts"
|
"./cloudflare": "./src/cloudflare-adapter.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
createClientChannel,
|
||||||
|
createDaemonChannel,
|
||||||
|
EncryptedChannel,
|
||||||
|
} from "./encrypted-channel.js";
|
||||||
|
export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
createClientChannel,
|
createClientChannel,
|
||||||
type EncryptedChannel,
|
type EncryptedChannel,
|
||||||
type Transport as RelayTransport,
|
type Transport as RelayTransport,
|
||||||
} from "@paseo/relay";
|
} from "@paseo/relay/e2ee";
|
||||||
|
|
||||||
export interface Logger {
|
export interface Logger {
|
||||||
debug(obj: object, msg?: string): void;
|
debug(obj: object, msg?: string): void;
|
||||||
@@ -319,14 +319,16 @@ export class DaemonClientV2 {
|
|||||||
createWebSocketTransportFactory(
|
createWebSocketTransportFactory(
|
||||||
this.config.webSocketFactory ?? defaultWebSocketFactory
|
this.config.webSocketFactory ?? defaultWebSocketFactory
|
||||||
);
|
);
|
||||||
const transportFactory =
|
const shouldUseRelayE2ee =
|
||||||
this.config.e2ee?.enabled === true
|
this.config.e2ee?.enabled === true &&
|
||||||
? createEncryptedTransportFactory(
|
isRelayClientWebSocketUrl(this.config.url);
|
||||||
baseTransportFactory,
|
const transportFactory = shouldUseRelayE2ee
|
||||||
this.config.e2ee?.daemonPublicKeyB64,
|
? createRelayE2eeTransportFactory(
|
||||||
this.logger
|
baseTransportFactory,
|
||||||
)
|
this.config.e2ee?.daemonPublicKeyB64,
|
||||||
: baseTransportFactory;
|
this.logger
|
||||||
|
)
|
||||||
|
: baseTransportFactory;
|
||||||
const transport = transportFactory({ url: this.config.url, headers });
|
const transport = transportFactory({ url: this.config.url, headers });
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
||||||
@@ -2342,7 +2344,7 @@ function createWebSocketTransportFactory(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEncryptedTransportFactory(
|
function createRelayE2eeTransportFactory(
|
||||||
baseFactory: DaemonTransportFactory,
|
baseFactory: DaemonTransportFactory,
|
||||||
daemonPublicKeyB64: string | undefined,
|
daemonPublicKeyB64: string | undefined,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
@@ -2357,6 +2359,18 @@ function createEncryptedTransportFactory(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRelayClientWebSocketUrl(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return (
|
||||||
|
parsed.searchParams.get("role") === "client" &&
|
||||||
|
parsed.searchParams.has("session")
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createEncryptedTransport(
|
function createEncryptedTransport(
|
||||||
base: DaemonTransport,
|
base: DaemonTransport,
|
||||||
daemonPublicKeyB64: string,
|
daemonPublicKeyB64: string,
|
||||||
|
|||||||
@@ -437,14 +437,13 @@ export async function createPaseoDaemon(
|
|||||||
const endpoints = buildOfferEndpoints({
|
const endpoints = buildOfferEndpoints({
|
||||||
listenHost: listenTarget.host,
|
listenHost: listenTarget.host,
|
||||||
port: listenTarget.port,
|
port: listenTarget.port,
|
||||||
relayEnabled,
|
|
||||||
relayEndpoint,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const offer = await createConnectionOfferV1({
|
const offer = await createConnectionOfferV1({
|
||||||
sessionId: connectionSessionId,
|
sessionId: connectionSessionId,
|
||||||
endpoints,
|
endpoints,
|
||||||
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
||||||
|
relay: relayEnabled ? { endpoint: relayEndpoint } : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const ConnectionOfferV1Schema = z.object({
|
import {
|
||||||
v: z.literal(1),
|
ConnectionOfferV1Schema,
|
||||||
sessionId: z.string().min(1),
|
type ConnectionOfferV1,
|
||||||
endpoints: z.array(z.string().min(1)).min(1),
|
} from "../shared/connection-offer.js";
|
||||||
daemonPublicKeyB64: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ConnectionOfferV1 = z.infer<typeof ConnectionOfferV1Schema>;
|
|
||||||
|
|
||||||
type BuildOfferEndpointsArgs = {
|
type BuildOfferEndpointsArgs = {
|
||||||
listenHost: string;
|
listenHost: string;
|
||||||
port: number;
|
port: number;
|
||||||
relayEnabled: boolean;
|
|
||||||
relayEndpoint: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildOfferEndpoints({
|
export function buildOfferEndpoints({
|
||||||
listenHost,
|
listenHost,
|
||||||
port,
|
port,
|
||||||
relayEnabled,
|
|
||||||
relayEndpoint,
|
|
||||||
}: BuildOfferEndpointsArgs): string[] {
|
}: BuildOfferEndpointsArgs): string[] {
|
||||||
const endpoints: string[] = [];
|
const endpoints: string[] = [];
|
||||||
|
|
||||||
@@ -41,10 +32,6 @@ export function buildOfferEndpoints({
|
|||||||
endpoints.push(`localhost:${port}`);
|
endpoints.push(`localhost:${port}`);
|
||||||
endpoints.push(`127.0.0.1:${port}`);
|
endpoints.push(`127.0.0.1:${port}`);
|
||||||
|
|
||||||
if (relayEnabled) {
|
|
||||||
endpoints.push(relayEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dedupePreserveOrder(endpoints);
|
return dedupePreserveOrder(endpoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +39,14 @@ export async function createConnectionOfferV1(args: {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
endpoints: string[];
|
endpoints: string[];
|
||||||
daemonPublicKeyB64: string;
|
daemonPublicKeyB64: string;
|
||||||
|
relay?: { endpoint: string } | null;
|
||||||
}): Promise<ConnectionOfferV1> {
|
}): Promise<ConnectionOfferV1> {
|
||||||
return ConnectionOfferV1Schema.parse({
|
return ConnectionOfferV1Schema.parse({
|
||||||
v: 1,
|
v: 1,
|
||||||
sessionId: args.sessionId,
|
sessionId: args.sessionId,
|
||||||
endpoints: args.endpoints,
|
endpoints: args.endpoints,
|
||||||
daemonPublicKeyB64: args.daemonPublicKeyB64,
|
daemonPublicKeyB64: args.daemonPublicKeyB64,
|
||||||
|
relay: args.relay ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ describe("ConnectionOfferV1 (daemon E2E)", () => {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
endpoints: string[];
|
endpoints: string[];
|
||||||
daemonPublicKeyB64: string;
|
daemonPublicKeyB64: string;
|
||||||
|
relay?: { endpoint: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(offer.v).toBe(1);
|
expect(offer.v).toBe(1);
|
||||||
@@ -97,7 +98,8 @@ describe("ConnectionOfferV1 (daemon E2E)", () => {
|
|||||||
expect(Array.isArray(offer.endpoints)).toBe(true);
|
expect(Array.isArray(offer.endpoints)).toBe(true);
|
||||||
expect(offer.endpoints).toContain(`192.168.1.12:${daemon.port}`);
|
expect(offer.endpoints).toContain(`192.168.1.12:${daemon.port}`);
|
||||||
expect(offer.endpoints).toContain(`localhost:${daemon.port}`);
|
expect(offer.endpoints).toContain(`localhost:${daemon.port}`);
|
||||||
expect(offer.endpoints).toContain("relay.paseo.sh:443");
|
expect(offer.endpoints).not.toContain("relay.paseo.sh:443");
|
||||||
|
expect(offer.relay?.endpoint).toBe("relay.paseo.sh:443");
|
||||||
expect(typeof offer.daemonPublicKeyB64).toBe("string");
|
expect(typeof offer.daemonPublicKeyB64).toBe("string");
|
||||||
expect(offer.daemonPublicKeyB64.length).toBeGreaterThan(0);
|
expect(offer.daemonPublicKeyB64.length).toBeGreaterThan(0);
|
||||||
expect(() => Buffer.from(offer.daemonPublicKeyB64, "base64")).not.toThrow();
|
expect(() => Buffer.from(offer.daemonPublicKeyB64, "base64")).not.toThrow();
|
||||||
@@ -167,9 +169,11 @@ describe("ConnectionOfferV1 (daemon E2E)", () => {
|
|||||||
|
|
||||||
const offer = decodeOfferFromFragmentUrl(offerUrl) as {
|
const offer = decodeOfferFromFragmentUrl(offerUrl) as {
|
||||||
endpoints: string[];
|
endpoints: string[];
|
||||||
|
relay?: { endpoint: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(offer.endpoints).not.toContain("relay.paseo.sh:443");
|
expect(offer.endpoints).not.toContain("relay.paseo.sh:443");
|
||||||
|
expect(offer.relay).toBe(null);
|
||||||
expect(offer.endpoints).toContain(`localhost:${port}`);
|
expect(offer.endpoints).toContain(`localhost:${port}`);
|
||||||
expect(offer.endpoints).toContain(`192.168.1.12:${port}`);
|
expect(offer.endpoints).toContain(`192.168.1.12:${port}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Writable } from "node:stream";
|
|||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
|
|
||||||
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
||||||
import { createClientChannel, type Transport } from "@paseo/relay";
|
import { createClientChannel, type Transport } from "@paseo/relay/e2ee";
|
||||||
import { createRelayServer } from "@paseo/relay/node";
|
import { createRelayServer } from "@paseo/relay/node";
|
||||||
|
|
||||||
function createCapturingLogger() {
|
function createCapturingLogger() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
createDaemonChannel,
|
createDaemonChannel,
|
||||||
type EncryptedChannel,
|
type EncryptedChannel,
|
||||||
type Transport as RelayTransport,
|
type Transport as RelayTransport,
|
||||||
} from "@paseo/relay";
|
} from "@paseo/relay/e2ee";
|
||||||
|
|
||||||
type RelayTransportOptions = {
|
type RelayTransportOptions = {
|
||||||
logger: pino.Logger;
|
logger: pino.Logger;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ConnectionOfferV1Schema = z.object({
|
||||||
|
v: z.literal(1),
|
||||||
|
sessionId: z.string().min(1),
|
||||||
|
endpoints: z.array(z.string().min(1)).min(1),
|
||||||
|
daemonPublicKeyB64: z.string().min(1),
|
||||||
|
relay: z
|
||||||
|
.object({
|
||||||
|
endpoint: z.string().min(1),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ConnectionOfferV1 = z.infer<typeof ConnectionOfferV1Schema>;
|
||||||
|
|
||||||
Reference in New Issue
Block a user