Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -43,7 +43,7 @@ function createMockState() {
|
||||
}
|
||||
|
||||
async function withMockWebSocketPair(
|
||||
run: (sockets: { clientWs: MockSocket; serverWs: MockSocket }) => Promise<void> | void
|
||||
run: (sockets: { clientWs: MockSocket; serverWs: MockSocket }) => Promise<void> | void,
|
||||
): Promise<void> {
|
||||
const serverWs = createMockSocket();
|
||||
const clientWs = createMockSocket();
|
||||
@@ -173,7 +173,7 @@ describe("RelayDurableObject control nudge/reset behavior", () => {
|
||||
headers: {
|
||||
Upgrade: "websocket",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await relay.fetch(req).catch(() => undefined);
|
||||
@@ -211,25 +211,27 @@ describe("RelayDurableObject control nudge/reset behavior", () => {
|
||||
disconnectedClient as unknown as WebSocket,
|
||||
1001,
|
||||
"Client disconnected",
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
expect(serverData.close).not.toHaveBeenCalled();
|
||||
expect(control.send).not.toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: "disconnected", connectionId: clientId })
|
||||
JSON.stringify({ type: "disconnected", connectionId: clientId }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relay worker endpoint routing", () => {
|
||||
it("routes missing v to legacy v1 isolated DO ids", async () => {
|
||||
const fetch = vi.fn(async (request: Request) => new Response(`ok:${new URL(request.url).searchParams.get("v")}`));
|
||||
const fetch = vi.fn(
|
||||
async (request: Request) => new Response(`ok:${new URL(request.url).searchParams.get("v")}`),
|
||||
);
|
||||
const get = vi.fn(() => ({ fetch }));
|
||||
const idFromName = vi.fn(() => ({ toString: () => "id" }));
|
||||
|
||||
const response = await relayWorker.fetch(
|
||||
new Request("https://relay.test/ws?serverId=srv_test&role=server"),
|
||||
{ RELAY: { idFromName, get } } as any
|
||||
{ RELAY: { idFromName, get } } as any,
|
||||
);
|
||||
|
||||
expect(idFromName).toHaveBeenCalledWith("relay-v1:srv_test");
|
||||
@@ -238,13 +240,15 @@ describe("relay worker endpoint routing", () => {
|
||||
});
|
||||
|
||||
it("routes v=2 to v2 isolated DO ids", async () => {
|
||||
const fetch = vi.fn(async (request: Request) => new Response(`ok:${new URL(request.url).searchParams.get("v")}`));
|
||||
const fetch = vi.fn(
|
||||
async (request: Request) => new Response(`ok:${new URL(request.url).searchParams.get("v")}`),
|
||||
);
|
||||
const get = vi.fn(() => ({ fetch }));
|
||||
const idFromName = vi.fn(() => ({ toString: () => "id" }));
|
||||
|
||||
const response = await relayWorker.fetch(
|
||||
new Request("https://relay.test/ws?serverId=srv_test&role=server&v=2"),
|
||||
{ RELAY: { idFromName, get } } as any
|
||||
{ RELAY: { idFromName, get } } as any,
|
||||
);
|
||||
|
||||
expect(idFromName).toHaveBeenCalledWith("relay-v2:srv_test");
|
||||
@@ -259,7 +263,7 @@ describe("relay worker endpoint routing", () => {
|
||||
|
||||
const response = await relayWorker.fetch(
|
||||
new Request("https://relay.test/ws?serverId=srv_test&role=server&v=nope"),
|
||||
{ RELAY: { idFromName, get } } as any
|
||||
{ RELAY: { idFromName, get } } as any,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
@@ -91,7 +91,9 @@ export class RelayDurableObject {
|
||||
}
|
||||
|
||||
private createWebSocketPair(): [WebSocket, WebSocket] {
|
||||
const pair = new (globalThis as unknown as { WebSocketPair: new () => WebSocketPair }).WebSocketPair();
|
||||
const pair = new (
|
||||
globalThis as unknown as { WebSocketPair: new () => WebSocketPair }
|
||||
).WebSocketPair();
|
||||
return [pair[0], pair[1]];
|
||||
}
|
||||
|
||||
@@ -188,8 +190,14 @@ export class RelayDurableObject {
|
||||
const out = new Set<string>();
|
||||
for (const ws of this.state.getWebSockets("client")) {
|
||||
try {
|
||||
const attachment = (ws as WebSocketWithAttachment).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
if (attachment?.role === "client" && typeof attachment.connectionId === "string" && attachment.connectionId) {
|
||||
const attachment = (
|
||||
ws as WebSocketWithAttachment
|
||||
).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
if (
|
||||
attachment?.role === "client" &&
|
||||
typeof attachment.connectionId === "string" &&
|
||||
attachment.connectionId
|
||||
) {
|
||||
out.add(attachment.connectionId);
|
||||
}
|
||||
} catch {
|
||||
@@ -244,7 +252,7 @@ export class RelayDurableObject {
|
||||
request: Request,
|
||||
role: ConnectionRole,
|
||||
serverId: string,
|
||||
connectionId: string
|
||||
connectionId: string,
|
||||
): Response {
|
||||
const upgradeError = this.requireWebSocketUpgrade(request);
|
||||
if (upgradeError) return upgradeError;
|
||||
@@ -295,7 +303,7 @@ export class RelayDurableObject {
|
||||
(server as WebSocketWithAttachment).serializeAttachment(attachment);
|
||||
|
||||
console.log(
|
||||
`[Relay DO] v2:${role}${isServerControl ? "(control)" : ""}${isServerData ? `(data:${resolvedConnectionId})` : role === "client" ? `(${resolvedConnectionId})` : ""} connected to session ${serverId}`
|
||||
`[Relay DO] v2:${role}${isServerControl ? "(control)" : ""}${isServerData ? `(data:${resolvedConnectionId})` : role === "client" ? `(${resolvedConnectionId})` : ""} connected to session ${serverId}`,
|
||||
);
|
||||
|
||||
if (role === "client") {
|
||||
@@ -306,7 +314,9 @@ export class RelayDurableObject {
|
||||
if (isServerControl) {
|
||||
// Send current connection list so the daemon can attach existing connections.
|
||||
try {
|
||||
server.send(JSON.stringify({ type: "sync", connectionIds: this.listConnectedConnectionIds() }));
|
||||
server.send(
|
||||
JSON.stringify({ type: "sync", connectionIds: this.listConnectedConnectionIds() }),
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -350,7 +360,9 @@ export class RelayDurableObject {
|
||||
* Called when a WebSocket message is received (wakes from hibernation).
|
||||
*/
|
||||
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {
|
||||
const attachment = (ws as WebSocketWithAttachment).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
const attachment = (
|
||||
ws as WebSocketWithAttachment
|
||||
).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
if (!attachment) {
|
||||
console.error("[Relay DO] Message from WebSocket without attachment");
|
||||
return;
|
||||
@@ -421,18 +433,15 @@ export class RelayDurableObject {
|
||||
/**
|
||||
* Called when a WebSocket closes (wakes from hibernation).
|
||||
*/
|
||||
webSocketClose(
|
||||
ws: WebSocket,
|
||||
code: number,
|
||||
reason: string,
|
||||
_wasClean: boolean
|
||||
): void {
|
||||
const attachment = (ws as WebSocketWithAttachment).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
webSocketClose(ws: WebSocket, code: number, reason: string, _wasClean: boolean): void {
|
||||
const attachment = (
|
||||
ws as WebSocketWithAttachment
|
||||
).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
if (!attachment) return;
|
||||
|
||||
const version = attachment.version ?? LEGACY_RELAY_VERSION;
|
||||
console.log(
|
||||
`[Relay DO] v${version}:${attachment.role}${attachment.connectionId ? `(${attachment.connectionId})` : ""} disconnected from session ${attachment.serverId} (${code}: ${reason})`
|
||||
`[Relay DO] v${version}:${attachment.role}${attachment.connectionId ? `(${attachment.connectionId})` : ""} disconnected from session ${attachment.serverId} (${code}: ${reason})`,
|
||||
);
|
||||
|
||||
if (version === LEGACY_RELAY_VERSION) {
|
||||
@@ -476,11 +485,10 @@ export class RelayDurableObject {
|
||||
* Called on WebSocket error.
|
||||
*/
|
||||
webSocketError(ws: WebSocket, error: unknown): void {
|
||||
const attachment = (ws as WebSocketWithAttachment).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
console.error(
|
||||
`[Relay DO] WebSocket error for ${attachment?.role ?? "unknown"}:`,
|
||||
error
|
||||
);
|
||||
const attachment = (
|
||||
ws as WebSocketWithAttachment
|
||||
).deserializeAttachment() as RelaySessionAttachment | null;
|
||||
console.error(`[Relay DO] WebSocket error for ${attachment?.role ?? "unknown"}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,11 @@ describe("crypto", () => {
|
||||
// Derive shared keys
|
||||
const daemonSharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
daemonSeesClientPubKey
|
||||
daemonSeesClientPubKey,
|
||||
);
|
||||
const clientSharedKey = await deriveSharedKey(
|
||||
clientKeyPair.secretKey,
|
||||
clientSeesDaemonPubKey
|
||||
clientSeesDaemonPubKey,
|
||||
);
|
||||
|
||||
// Both should derive the same key - test by encrypting with one, decrypting with other
|
||||
@@ -71,10 +71,7 @@ describe("crypto", () => {
|
||||
it("roundtrips a string message", async () => {
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
const clientKeyPair = await generateKeyPair();
|
||||
const sharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientKeyPair.publicKey
|
||||
);
|
||||
const sharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientKeyPair.publicKey);
|
||||
|
||||
const plaintext = "Test message with unicode: 你好世界 🎉";
|
||||
const ciphertext = await encrypt(sharedKey, plaintext);
|
||||
@@ -89,10 +86,7 @@ describe("crypto", () => {
|
||||
it("roundtrips binary data", async () => {
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
const clientKeyPair = await generateKeyPair();
|
||||
const sharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientKeyPair.publicKey
|
||||
);
|
||||
const sharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientKeyPair.publicKey);
|
||||
|
||||
const binary = new Uint8Array([0, 1, 2, 255, 254, 253]);
|
||||
const ciphertext = await encrypt(sharedKey, binary.buffer);
|
||||
@@ -106,14 +100,8 @@ describe("crypto", () => {
|
||||
const keypair2 = await generateKeyPair();
|
||||
const keypair3 = await generateKeyPair();
|
||||
|
||||
const correctKey = await deriveSharedKey(
|
||||
keypair1.secretKey,
|
||||
keypair2.publicKey
|
||||
);
|
||||
const wrongKey = await deriveSharedKey(
|
||||
keypair1.secretKey,
|
||||
keypair3.publicKey
|
||||
);
|
||||
const correctKey = await deriveSharedKey(keypair1.secretKey, keypair2.publicKey);
|
||||
const wrongKey = await deriveSharedKey(keypair1.secretKey, keypair3.publicKey);
|
||||
|
||||
const ciphertext = await encrypt(correctKey, "secret");
|
||||
|
||||
@@ -123,10 +111,7 @@ describe("crypto", () => {
|
||||
it("produces different ciphertext for same plaintext (random IV)", async () => {
|
||||
const keypair1 = await generateKeyPair();
|
||||
const keypair2 = await generateKeyPair();
|
||||
const sharedKey = await deriveSharedKey(
|
||||
keypair1.secretKey,
|
||||
keypair2.publicKey
|
||||
);
|
||||
const sharedKey = await deriveSharedKey(keypair1.secretKey, keypair2.publicKey);
|
||||
|
||||
const plaintext = "Same message";
|
||||
const ciphertext1 = await encrypt(sharedKey, plaintext);
|
||||
@@ -159,10 +144,7 @@ describe("crypto", () => {
|
||||
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
|
||||
|
||||
// Client derives shared key
|
||||
const clientSharedKey = await deriveSharedKey(
|
||||
clientKeyPair.secretKey,
|
||||
daemonPubKeyOnClient
|
||||
);
|
||||
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
|
||||
|
||||
// Client sends hello: { type: "hello", key: clientPubKeyB64 }
|
||||
|
||||
@@ -171,10 +153,7 @@ describe("crypto", () => {
|
||||
const clientPubKeyOnDaemon = await importPublicKey(clientPubKeyB64);
|
||||
|
||||
// Daemon derives shared key
|
||||
const daemonSharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientPubKeyOnDaemon
|
||||
);
|
||||
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
|
||||
|
||||
// === VERIFY BOTH HAVE SAME KEY ===
|
||||
const testFromDaemon = "Message from daemon";
|
||||
@@ -182,15 +161,11 @@ describe("crypto", () => {
|
||||
|
||||
// Daemon encrypts, client decrypts
|
||||
const encryptedFromDaemon = await encrypt(daemonSharedKey, testFromDaemon);
|
||||
expect(await decrypt(clientSharedKey, encryptedFromDaemon)).toBe(
|
||||
testFromDaemon
|
||||
);
|
||||
expect(await decrypt(clientSharedKey, encryptedFromDaemon)).toBe(testFromDaemon);
|
||||
|
||||
// Client encrypts, daemon decrypts
|
||||
const encryptedFromClient = await encrypt(clientSharedKey, testFromClient);
|
||||
expect(await decrypt(daemonSharedKey, encryptedFromClient)).toBe(
|
||||
testFromClient
|
||||
);
|
||||
expect(await decrypt(daemonSharedKey, encryptedFromClient)).toBe(testFromClient);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,10 +103,7 @@ export function importSecretKey(base64: string): Uint8Array {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function deriveSharedKey(
|
||||
ourSecretKey: Uint8Array,
|
||||
peerPublicKey: Uint8Array
|
||||
): SharedKey {
|
||||
export function deriveSharedKey(ourSecretKey: Uint8Array, peerPublicKey: Uint8Array): SharedKey {
|
||||
if (ourSecretKey.byteLength !== nacl.box.secretKeyLength) {
|
||||
throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
|
||||
}
|
||||
|
||||
@@ -57,17 +57,23 @@ function spawnRelayDevServer(port: number): ChildProcess {
|
||||
env: { ...process.env },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function assertRelayStillRunning(relayProcess: ChildProcess): void {
|
||||
if (relayProcess.exitCode !== null) {
|
||||
throw new Error(`relay process exited before startup completed (code: ${relayProcess.exitCode})`);
|
||||
throw new Error(
|
||||
`relay process exited before startup completed (code: ${relayProcess.exitCode})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForServer(port: number, relayProcess: ChildProcess, timeout = 15000): Promise<void> {
|
||||
async function waitForServer(
|
||||
port: number,
|
||||
relayProcess: ChildProcess,
|
||||
timeout = 15000,
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
assertRelayStillRunning(relayProcess);
|
||||
@@ -90,7 +96,7 @@ async function waitForServer(port: number, relayProcess: ChildProcess, timeout =
|
||||
async function waitForRelayWebSocketReady(
|
||||
port: number,
|
||||
relayProcess: ChildProcess,
|
||||
timeout = 60000
|
||||
timeout = 60000,
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
@@ -156,14 +162,20 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
relayProcess = spawnRelayDevServer(relayPort);
|
||||
|
||||
relayProcess.stdout?.on("data", (data: Buffer) => {
|
||||
const lines = data.toString().split("\n").filter((l) => l.trim());
|
||||
const lines = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((l) => l.trim());
|
||||
for (const line of lines) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[relay] ${line}`);
|
||||
}
|
||||
});
|
||||
relayProcess.stderr?.on("data", (data: Buffer) => {
|
||||
const lines = data.toString().split("\n").filter((l) => l.trim());
|
||||
const lines = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((l) => l.trim());
|
||||
for (const line of lines) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[relay] ${line}`);
|
||||
@@ -187,7 +199,9 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
}
|
||||
}, SHUTDOWN_TIMEOUT_MS);
|
||||
|
||||
it("full flow: daemon and client exchange encrypted messages through relay", { timeout: 90_000 }, async () => {
|
||||
it("full flow: daemon and client exchange encrypted messages through relay", {
|
||||
timeout: 90_000,
|
||||
}, async () => {
|
||||
const serverId = "test-session-" + Date.now();
|
||||
const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2);
|
||||
|
||||
@@ -200,7 +214,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
|
||||
// Daemon connects to relay as "server" control role
|
||||
const daemonControlWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -216,16 +230,10 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
|
||||
// Client imports daemon's public key and derives shared secret
|
||||
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
|
||||
const clientSharedKey = await deriveSharedKey(
|
||||
clientKeyPair.secretKey,
|
||||
daemonPubKeyOnClient
|
||||
);
|
||||
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
|
||||
|
||||
const waitForClientSeen = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("timed out waiting for connected")),
|
||||
5000
|
||||
);
|
||||
const timeout = setTimeout(() => reject(new Error("timed out waiting for connected")), 5000);
|
||||
const onMessage = (raw: unknown) => {
|
||||
try {
|
||||
const text =
|
||||
@@ -241,7 +249,11 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (msg?.type === "sync" && Array.isArray(msg.connectionIds) && msg.connectionIds.includes(connectionId)) {
|
||||
if (
|
||||
msg?.type === "sync" &&
|
||||
Array.isArray(msg.connectionIds) &&
|
||||
msg.connectionIds.includes(connectionId)
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
daemonControlWs.off("message", onMessage);
|
||||
resolve();
|
||||
@@ -255,7 +267,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
|
||||
// Client connects to relay as "client" role (must include connectionId)
|
||||
const clientWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -266,7 +278,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
await waitForClientSeen;
|
||||
|
||||
const daemonWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
daemonWs.on("open", resolve);
|
||||
@@ -288,10 +300,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
|
||||
// Daemon imports client's public key and derives shared secret
|
||||
const clientPubKeyOnDaemon = await importPublicKey(hello.key);
|
||||
const daemonSharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientPubKeyOnDaemon
|
||||
);
|
||||
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
|
||||
|
||||
// === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages ===
|
||||
|
||||
@@ -308,8 +317,8 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
clientSharedKey,
|
||||
clientReceivedReady.buffer.slice(
|
||||
clientReceivedReady.byteOffset,
|
||||
clientReceivedReady.byteOffset + clientReceivedReady.byteLength
|
||||
)
|
||||
clientReceivedReady.byteOffset + clientReceivedReady.byteLength,
|
||||
),
|
||||
);
|
||||
expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" });
|
||||
|
||||
@@ -326,8 +335,8 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
daemonSharedKey,
|
||||
daemonReceivedMsg.buffer.slice(
|
||||
daemonReceivedMsg.byteOffset,
|
||||
daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength
|
||||
)
|
||||
daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedClientMsg).toBe(clientMessage);
|
||||
|
||||
@@ -344,8 +353,8 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
clientSharedKey,
|
||||
clientReceivedMsg.buffer.slice(
|
||||
clientReceivedMsg.byteOffset,
|
||||
clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength
|
||||
)
|
||||
clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedDaemonMsg).toBe(daemonMessage);
|
||||
|
||||
@@ -368,25 +377,16 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
const clientPubKey = await importPublicKey(clientPubKeyB64);
|
||||
const daemonPubKey = await importPublicKey(daemonPubKeyB64);
|
||||
|
||||
const daemonSharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientPubKey
|
||||
);
|
||||
const clientSharedKey = await deriveSharedKey(
|
||||
clientKeyPair.secretKey,
|
||||
daemonPubKey
|
||||
);
|
||||
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKey);
|
||||
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKey);
|
||||
|
||||
const daemonControlWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
|
||||
);
|
||||
await new Promise<void>((r) => daemonControlWs.on("open", r));
|
||||
|
||||
const waitForClientSeen = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("timed out waiting for connected")),
|
||||
5000
|
||||
);
|
||||
const timeout = setTimeout(() => reject(new Error("timed out waiting for connected")), 5000);
|
||||
const onMessage = (raw: unknown) => {
|
||||
try {
|
||||
const text =
|
||||
@@ -402,7 +402,11 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (msg?.type === "sync" && Array.isArray(msg.connectionIds) && msg.connectionIds.includes(connectionId)) {
|
||||
if (
|
||||
msg?.type === "sync" &&
|
||||
Array.isArray(msg.connectionIds) &&
|
||||
msg.connectionIds.includes(connectionId)
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
daemonControlWs.off("message", onMessage);
|
||||
resolve();
|
||||
@@ -415,13 +419,13 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
});
|
||||
|
||||
const clientWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
await new Promise<void>((r) => clientWs.on("open", r));
|
||||
await waitForClientSeen;
|
||||
|
||||
const daemonWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
await new Promise<void>((r) => daemonWs.on("open", r));
|
||||
|
||||
@@ -448,10 +452,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
// But daemon can decrypt
|
||||
const decrypted = await decrypt(
|
||||
daemonSharedKey,
|
||||
received.buffer.slice(
|
||||
received.byteOffset,
|
||||
received.byteOffset + received.byteLength
|
||||
)
|
||||
received.buffer.slice(received.byteOffset, received.byteOffset + received.byteLength),
|
||||
);
|
||||
expect(decrypted).toBe(secret);
|
||||
|
||||
@@ -468,22 +469,12 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
const clientKeyPair = await generateKeyPair();
|
||||
const attackerKeyPair = await generateKeyPair();
|
||||
|
||||
const clientPubKey = await importPublicKey(
|
||||
await exportPublicKey(clientKeyPair.publicKey)
|
||||
);
|
||||
const daemonSharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientPubKey
|
||||
);
|
||||
const clientPubKey = await importPublicKey(await exportPublicKey(clientKeyPair.publicKey));
|
||||
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKey);
|
||||
|
||||
// Attacker tries to derive key with their own keypair
|
||||
const attackerPubKey = await importPublicKey(
|
||||
await exportPublicKey(attackerKeyPair.publicKey)
|
||||
);
|
||||
const attackerKey = await deriveSharedKey(
|
||||
attackerKeyPair.secretKey,
|
||||
attackerPubKey
|
||||
);
|
||||
const attackerPubKey = await importPublicKey(await exportPublicKey(attackerKeyPair.publicKey));
|
||||
const attackerKey = await deriveSharedKey(attackerKeyPair.secretKey, attackerPubKey);
|
||||
|
||||
// Encrypt with daemon's key
|
||||
const secret = "Top secret message";
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
createClientChannel,
|
||||
createDaemonChannel,
|
||||
Transport,
|
||||
} from "./encrypted-channel.js";
|
||||
import { createClientChannel, createDaemonChannel, Transport } from "./encrypted-channel.js";
|
||||
import { generateKeyPair, exportPublicKey } from "./crypto.js";
|
||||
|
||||
/**
|
||||
@@ -28,17 +24,13 @@ function createMockTransportPair(): [Transport, Transport] {
|
||||
};
|
||||
|
||||
// Wire them together
|
||||
(transportA.send as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
(data: string | ArrayBuffer) => {
|
||||
setTimeout(() => transportB.onmessage?.(data), 0);
|
||||
}
|
||||
);
|
||||
(transportA.send as ReturnType<typeof vi.fn>).mockImplementation((data: string | ArrayBuffer) => {
|
||||
setTimeout(() => transportB.onmessage?.(data), 0);
|
||||
});
|
||||
|
||||
(transportB.send as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
(data: string | ArrayBuffer) => {
|
||||
setTimeout(() => transportA.onmessage?.(data), 0);
|
||||
}
|
||||
);
|
||||
(transportB.send as ReturnType<typeof vi.fn>).mockImplementation((data: string | ArrayBuffer) => {
|
||||
setTimeout(() => transportA.onmessage?.(data), 0);
|
||||
});
|
||||
|
||||
return [transportA, transportB];
|
||||
}
|
||||
@@ -60,11 +52,9 @@ describe("EncryptedChannel", () => {
|
||||
const daemonChannelPromise = createDaemonChannel(daemonTransport, daemonKeyPair);
|
||||
|
||||
// Client connects (scanned QR, got daemon's public key)
|
||||
const clientChannel = await createClientChannel(
|
||||
clientTransport,
|
||||
daemonPubKeyB64,
|
||||
{ onopen: () => clientOpenedResolve?.() }
|
||||
);
|
||||
const clientChannel = await createClientChannel(clientTransport, daemonPubKeyB64, {
|
||||
onopen: () => clientOpenedResolve?.(),
|
||||
});
|
||||
|
||||
// Daemon receives hello and completes handshake
|
||||
const daemonChannel = await daemonChannelPromise;
|
||||
@@ -88,17 +78,14 @@ describe("EncryptedChannel", () => {
|
||||
clientOpenedResolve = resolve;
|
||||
});
|
||||
|
||||
const daemonChannelPromise = createDaemonChannel(
|
||||
daemonTransport,
|
||||
daemonKeyPair,
|
||||
{ onmessage: (data) => daemonMessages.push(data) }
|
||||
);
|
||||
const daemonChannelPromise = createDaemonChannel(daemonTransport, daemonKeyPair, {
|
||||
onmessage: (data) => daemonMessages.push(data),
|
||||
});
|
||||
|
||||
const clientChannel = await createClientChannel(
|
||||
clientTransport,
|
||||
daemonPubKeyB64,
|
||||
{ onmessage: (data) => clientMessages.push(data), onopen: () => clientOpenedResolve?.() }
|
||||
);
|
||||
const clientChannel = await createClientChannel(clientTransport, daemonPubKeyB64, {
|
||||
onmessage: (data) => clientMessages.push(data),
|
||||
onopen: () => clientOpenedResolve?.(),
|
||||
});
|
||||
|
||||
const daemonChannel = await daemonChannelPromise;
|
||||
await clientOpened;
|
||||
@@ -111,10 +98,7 @@ describe("EncryptedChannel", () => {
|
||||
// Wait for async delivery
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
expect(daemonMessages).toEqual([
|
||||
"Hello from client",
|
||||
"Second message from client",
|
||||
]);
|
||||
expect(daemonMessages).toEqual(["Hello from client", "Second message from client"]);
|
||||
expect(clientMessages).toEqual(["Hello from daemon"]);
|
||||
});
|
||||
|
||||
@@ -130,11 +114,9 @@ describe("EncryptedChannel", () => {
|
||||
});
|
||||
|
||||
const daemonChannelPromise = createDaemonChannel(daemonTransport, daemonKeyPair);
|
||||
const clientChannel = await createClientChannel(
|
||||
clientTransport,
|
||||
daemonPubKeyB64,
|
||||
{ onopen: () => clientOpenedResolve?.() }
|
||||
);
|
||||
const clientChannel = await createClientChannel(clientTransport, daemonPubKeyB64, {
|
||||
onopen: () => clientOpenedResolve?.(),
|
||||
});
|
||||
await daemonChannelPromise;
|
||||
await clientOpened;
|
||||
|
||||
@@ -147,8 +129,7 @@ describe("EncryptedChannel", () => {
|
||||
|
||||
// Check what was actually sent over the transport
|
||||
expect(clientTransport.send).toHaveBeenCalledTimes(1);
|
||||
const sentData = (clientTransport.send as ReturnType<typeof vi.fn>).mock
|
||||
.calls[0][0];
|
||||
const sentData = (clientTransport.send as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
|
||||
// Should be base64 string (encrypted)
|
||||
expect(typeof sentData).toBe("string");
|
||||
@@ -189,9 +170,7 @@ describe("EncryptedChannel", () => {
|
||||
|
||||
expect(onerror).toHaveBeenCalledTimes(1);
|
||||
expect(onerror.mock.calls[0][0]).toBeInstanceOf(Error);
|
||||
expect((onerror.mock.calls[0][0] as Error).message).toContain(
|
||||
"WebSocket not open"
|
||||
);
|
||||
expect((onerror.mock.calls[0][0] as Error).message).toContain("WebSocket not open");
|
||||
|
||||
// Close the transport to stop retry timer.
|
||||
transport.onclose?.(1000, "closed");
|
||||
|
||||
@@ -58,18 +58,15 @@ interface E2EEReadyMessage {
|
||||
|
||||
function buildInvalidHelloError(rawText: string, parsed?: unknown): Error {
|
||||
const parsedRecord =
|
||||
parsed && typeof parsed === "object"
|
||||
? (parsed as Record<string, unknown>)
|
||||
: null;
|
||||
parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
|
||||
const rawType = parsedRecord?.type;
|
||||
const receivedType =
|
||||
typeof rawType === "string" ? rawType : rawType === undefined ? "undefined" : typeof rawType;
|
||||
const hasKey =
|
||||
typeof parsedRecord?.key === "string" && parsedRecord.key.trim().length > 0;
|
||||
const hasKey = typeof parsedRecord?.key === "string" && parsedRecord.key.trim().length > 0;
|
||||
const compact = rawText.replace(/\s+/g, " ").trim();
|
||||
const preview = compact.length > 160 ? `${compact.slice(0, 157)}...` : compact;
|
||||
return new Error(
|
||||
`Invalid hello message (receivedType=${receivedType}, hasKey=${hasKey}, preview=${JSON.stringify(preview)})`
|
||||
`Invalid hello message (receivedType=${receivedType}, hasKey=${hasKey}, preview=${JSON.stringify(preview)})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +85,7 @@ const MAX_PENDING_SENDS = 200;
|
||||
export async function createClientChannel(
|
||||
transport: Transport,
|
||||
daemonPublicKeyB64: string,
|
||||
events: EncryptedChannelEvents = {}
|
||||
events: EncryptedChannelEvents = {},
|
||||
): Promise<EncryptedChannel> {
|
||||
const keyPair = generateKeyPair();
|
||||
const daemonPublicKey = importPublicKey(daemonPublicKeyB64);
|
||||
@@ -152,7 +149,7 @@ export async function createClientChannel(
|
||||
export async function createDaemonChannel(
|
||||
transport: Transport,
|
||||
daemonKeyPair: KeyPair,
|
||||
events: EncryptedChannelEvents = {}
|
||||
events: EncryptedChannelEvents = {},
|
||||
): Promise<EncryptedChannel> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const bufferedMessages: Array<string | ArrayBuffer> = [];
|
||||
@@ -168,8 +165,7 @@ export async function createDaemonChannel(
|
||||
|
||||
transport.onmessage = async (data) => {
|
||||
try {
|
||||
const helloText =
|
||||
typeof data === "string" ? data : new TextDecoder().decode(data);
|
||||
const helloText = typeof data === "string" ? data : new TextDecoder().decode(data);
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
@@ -195,9 +191,7 @@ export async function createDaemonChannel(
|
||||
const sharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPublicKey);
|
||||
|
||||
const channel = new EncryptedChannel(transport, sharedKey, events, { daemonKeyPair });
|
||||
transport.send(
|
||||
JSON.stringify({ type: "e2ee_ready" } satisfies E2EEReadyMessage)
|
||||
);
|
||||
transport.send(JSON.stringify({ type: "e2ee_ready" } satisfies E2EEReadyMessage));
|
||||
|
||||
channel.setState("open");
|
||||
events.onopen?.();
|
||||
@@ -240,7 +234,7 @@ export class EncryptedChannel {
|
||||
transport: Transport,
|
||||
sharedKey: SharedKey,
|
||||
events: EncryptedChannelEvents = {},
|
||||
options: EncryptedChannelOptions = {}
|
||||
options: EncryptedChannelOptions = {},
|
||||
) {
|
||||
this.transport = transport;
|
||||
this.sharedKey = sharedKey;
|
||||
@@ -287,9 +281,7 @@ export class EncryptedChannel {
|
||||
try {
|
||||
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
||||
if (text.trim().startsWith("{")) {
|
||||
const parsed = JSON.parse(text) as Partial<
|
||||
E2EEHelloMessage | E2EEReadyMessage
|
||||
>;
|
||||
const parsed = JSON.parse(text) as Partial<E2EEHelloMessage | E2EEReadyMessage>;
|
||||
|
||||
if (parsed.type === "e2ee_hello" && typeof parsed.key === "string") {
|
||||
if (this.options.daemonKeyPair) {
|
||||
@@ -297,7 +289,7 @@ export class EncryptedChannel {
|
||||
const clientPublicKey = importPublicKey(parsed.key);
|
||||
const nextSharedKey = deriveSharedKey(
|
||||
this.options.daemonKeyPair.secretKey,
|
||||
clientPublicKey
|
||||
clientPublicKey,
|
||||
);
|
||||
|
||||
// If it's the same client key (handshake retry), re-send
|
||||
@@ -305,7 +297,7 @@ export class EncryptedChannel {
|
||||
// the channel and cause decrypt failures.
|
||||
if (keysEqual(nextSharedKey, this.sharedKey)) {
|
||||
this.transport.send(
|
||||
JSON.stringify({ type: "e2ee_ready" } satisfies E2EEReadyMessage)
|
||||
JSON.stringify({ type: "e2ee_ready" } satisfies E2EEReadyMessage),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -318,7 +310,7 @@ export class EncryptedChannel {
|
||||
this.sharedKey = nextSharedKey;
|
||||
this.pendingSends = [];
|
||||
this.transport.send(
|
||||
JSON.stringify({ type: "e2ee_ready" } satisfies E2EEReadyMessage)
|
||||
JSON.stringify({ type: "e2ee_ready" } satisfies E2EEReadyMessage),
|
||||
);
|
||||
this.state = "open";
|
||||
await this.flushPendingSends();
|
||||
|
||||
@@ -13,7 +13,7 @@ const RELAY_BASE_URL = "wss://relay.paseo.sh";
|
||||
|
||||
async function withRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: { retries: number; delayMs: number }
|
||||
options: { retries: number; delayMs: number },
|
||||
): Promise<T> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt <= options.retries; attempt++) {
|
||||
@@ -39,10 +39,10 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
const connectionId = `clt_live_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
const serverControlUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(serverId)}&role=server&v=2`;
|
||||
const serverDataUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(
|
||||
serverId
|
||||
serverId,
|
||||
)}&role=server&connectionId=${encodeURIComponent(connectionId)}&v=2`;
|
||||
const clientUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(
|
||||
serverId
|
||||
serverId,
|
||||
)}&role=client&connectionId=${encodeURIComponent(connectionId)}&v=2`;
|
||||
|
||||
// === Key setup ===
|
||||
@@ -55,7 +55,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
|
||||
const clientSharedKey = await deriveSharedKey(
|
||||
clientKeyPair.secretKey,
|
||||
daemonPubKeyOnClient
|
||||
daemonPubKeyOnClient,
|
||||
);
|
||||
|
||||
// === Connect ===
|
||||
@@ -67,7 +67,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error(`Timed out opening ${label} websocket`)),
|
||||
10_000
|
||||
10_000,
|
||||
);
|
||||
ws.once("open", () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -88,7 +88,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("Timed out waiting for connected")),
|
||||
10_000
|
||||
10_000,
|
||||
);
|
||||
daemonControlWs.on("message", (raw) => {
|
||||
try {
|
||||
@@ -113,7 +113,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
const daemonReceivedHello = await new Promise<string>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("Timed out waiting for hello")),
|
||||
10_000
|
||||
10_000,
|
||||
);
|
||||
daemonWs!.once("message", (data) => {
|
||||
clearTimeout(timeout);
|
||||
@@ -131,67 +131,55 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
const clientPubKeyOnDaemon = await importPublicKey(hello.key!);
|
||||
const daemonSharedKey = await deriveSharedKey(
|
||||
daemonKeyPair.secretKey,
|
||||
clientPubKeyOnDaemon
|
||||
clientPubKeyOnDaemon,
|
||||
);
|
||||
|
||||
// === Encrypted exchange ===
|
||||
const plaintextFromClient = "hello-from-client";
|
||||
const ciphertextFromClient = await encrypt(
|
||||
clientSharedKey,
|
||||
plaintextFromClient
|
||||
);
|
||||
const ciphertextFromClient = await encrypt(clientSharedKey, plaintextFromClient);
|
||||
clientWs.send(Buffer.from(ciphertextFromClient));
|
||||
|
||||
const daemonReceivedCiphertext = await new Promise<Buffer>(
|
||||
(resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("Timed out waiting for encrypted message")),
|
||||
10_000
|
||||
);
|
||||
daemonWs!.once("message", (data) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(data as Buffer);
|
||||
});
|
||||
}
|
||||
);
|
||||
const daemonReceivedCiphertext = await new Promise<Buffer>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("Timed out waiting for encrypted message")),
|
||||
10_000,
|
||||
);
|
||||
daemonWs!.once("message", (data) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(data as Buffer);
|
||||
});
|
||||
});
|
||||
|
||||
const decryptedOnDaemon = await decrypt(
|
||||
daemonSharedKey,
|
||||
daemonReceivedCiphertext.buffer.slice(
|
||||
daemonReceivedCiphertext.byteOffset,
|
||||
daemonReceivedCiphertext.byteOffset +
|
||||
daemonReceivedCiphertext.byteLength
|
||||
)
|
||||
daemonReceivedCiphertext.byteOffset + daemonReceivedCiphertext.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedOnDaemon).toBe(plaintextFromClient);
|
||||
|
||||
const plaintextFromDaemon = "hello-from-daemon";
|
||||
const ciphertextFromDaemon = await encrypt(
|
||||
daemonSharedKey,
|
||||
plaintextFromDaemon
|
||||
);
|
||||
const ciphertextFromDaemon = await encrypt(daemonSharedKey, plaintextFromDaemon);
|
||||
daemonWs!.send(Buffer.from(ciphertextFromDaemon));
|
||||
|
||||
const clientReceivedCiphertext = await new Promise<Buffer>(
|
||||
(resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("Timed out waiting for encrypted response")),
|
||||
10_000
|
||||
);
|
||||
clientWs.once("message", (data) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(data as Buffer);
|
||||
});
|
||||
}
|
||||
);
|
||||
const clientReceivedCiphertext = await new Promise<Buffer>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("Timed out waiting for encrypted response")),
|
||||
10_000,
|
||||
);
|
||||
clientWs.once("message", (data) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(data as Buffer);
|
||||
});
|
||||
});
|
||||
|
||||
const decryptedOnClient = await decrypt(
|
||||
clientSharedKey,
|
||||
clientReceivedCiphertext.buffer.slice(
|
||||
clientReceivedCiphertext.byteOffset,
|
||||
clientReceivedCiphertext.byteOffset +
|
||||
clientReceivedCiphertext.byteLength
|
||||
)
|
||||
clientReceivedCiphertext.byteOffset + clientReceivedCiphertext.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedOnClient).toBe(plaintextFromDaemon);
|
||||
} finally {
|
||||
@@ -200,7 +188,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
clientWs.close();
|
||||
}
|
||||
},
|
||||
{ retries: 2, delayMs: 250 }
|
||||
{ retries: 2, delayMs: 250 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user