refactor(relay): fix type-aware lint errors in WebSocket and crypto handling (#698)

This commit is contained in:
Mohamed Boudra
2026-05-05 19:56:52 +07:00
parent 400934f19c
commit f974812dfc
3 changed files with 139 additions and 47 deletions
+44 -12
View File
@@ -56,9 +56,25 @@ interface E2EEReadyMessage {
type: "e2ee_ready";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isE2EEHelloMessage(value: unknown): value is E2EEHelloMessage {
return (
isRecord(value) &&
value.type === "e2ee_hello" &&
typeof value.key === "string" &&
value.key.trim().length > 0
);
}
function isE2EEReadyMessage(value: unknown): value is E2EEReadyMessage {
return isRecord(value) && value.type === "e2ee_ready";
}
function buildInvalidHelloError(rawText: string, parsed?: unknown): Error {
const parsedRecord =
parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
const parsedRecord = isRecord(parsed) ? parsed : null;
const rawType = parsedRecord?.type;
function describeType(value: unknown): string {
if (typeof value === "string") return value;
@@ -77,6 +93,19 @@ function buildInvalidHelloError(rawText: string, parsed?: unknown): Error {
const HANDSHAKE_RETRY_MS = 1000;
const MAX_PENDING_SENDS = 200;
interface TimeoutWithUnref {
unref(): void;
}
function hasUnref(timeout: unknown): timeout is TimeoutWithUnref {
return (
typeof timeout === "object" &&
timeout !== null &&
"unref" in timeout &&
typeof (timeout as Record<string, unknown>).unref === "function"
);
}
/**
* Creates an encrypted channel as the initiator (client).
*
@@ -137,7 +166,9 @@ export async function createClientChannel(
sendHello();
}, HANDSHAKE_RETRY_MS);
// Avoid keeping Node processes alive (e.g. tests) if the handshake is stuck.
(retry as unknown as { unref?: () => void }).unref?.();
if (hasUnref(retry)) {
retry.unref();
}
return channel;
}
@@ -160,8 +191,8 @@ export async function createDaemonChannel(
const shouldIgnorePostHelloPlaintext = (data: string | ArrayBuffer): boolean => {
try {
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
const parsed = JSON.parse(text) as Partial<E2EEHelloMessage | E2EEReadyMessage>;
return parsed.type === "e2ee_hello" || parsed.type === "e2ee_ready";
const parsed: unknown = JSON.parse(text);
return isE2EEHelloMessage(parsed) || isE2EEReadyMessage(parsed);
} catch {
return false;
}
@@ -178,11 +209,12 @@ export async function createDaemonChannel(
throw buildInvalidHelloError(helloText);
}
const msg = parsed as Partial<E2EEHelloMessage>;
if (msg.type !== "e2ee_hello" || typeof msg.key !== "string" || !msg.key.trim()) {
if (!isE2EEHelloMessage(parsed)) {
throw buildInvalidHelloError(helloText, parsed);
}
const msg = parsed;
// Buffer any subsequent messages that arrive while we're doing async
// WebCrypto work to derive the shared key. Without this, it's possible
// for the next message (already encrypted) to be misinterpreted as a
@@ -269,8 +301,8 @@ export class EncryptedChannel {
if (this.state === "handshaking") {
try {
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
const msg = JSON.parse(text) as Partial<E2EEReadyMessage>;
if (msg.type === "e2ee_ready") {
const parsed: unknown = JSON.parse(text);
if (isE2EEReadyMessage(parsed)) {
this.state = "open";
this.events.onopen?.();
for (const cb of this.onOpenCallbacks) cb();
@@ -290,16 +322,16 @@ 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: unknown = JSON.parse(text);
if (parsed.type === "e2ee_hello" && typeof parsed.key === "string") {
if (isE2EEHelloMessage(parsed)) {
if (this.options.daemonKeyPair) {
await this.handleDaemonRehello(parsed.key);
}
return null;
}
if (parsed.type === "e2ee_ready") {
if (isE2EEReadyMessage(parsed)) {
return null;
}