// Client for signatureVerifyWorker. Verifies event batches off the main thread // and resolves with the Set of valid event ids. Falls back to main-thread // verification only when Web Workers are unavailable, so signatures are still // checked (never silently trusted) in that degraded case. import { devLogger } from "../utils/logger.js"; import { ensureNostrTools, getCachedNostrTools } from "./toolkit.js"; // Short timeout: if the worker can't answer quickly (e.g. nostr-tools is slow to // load inside the worker), we must NOT stall the feed — fall back to the // already-loaded main-thread tools instead. After one failure we stop using the // worker entirely for the session so we never pay the timeout twice. const DEFAULT_TIMEOUT_MS = 2500; let workerInstance = null; let workerReady = false; let workerDisabled = false; let requestId = 0; const pending = new Map(); function attachWorkerListeners(worker) { if (!worker || workerReady) return; worker.addEventListener("message", (event) => { const data = event?.data || {}; const entry = pending.get(data.id); if (!entry) return; pending.delete(data.id); clearTimeout(entry.timeoutId); if (data.ok && Array.isArray(data.validIds)) { entry.resolve(new Set(data.validIds)); } else { entry.reject(new Error(data?.error?.message || "verify-worker-error")); } }); worker.addEventListener("error", (error) => { devLogger.warn("[signatureVerifyWorkerClient] Worker error", error); disableWorker(); for (const entry of pending.values()) { clearTimeout(entry.timeoutId); entry.reject(error instanceof Error ? error : new Error("verify-worker-error")); } pending.clear(); }); workerReady = true; } // Stop using the worker for the rest of the session (it hung or errored). All // subsequent verification goes straight to the main-thread tools, which are // already loaded by the app. function disableWorker() { workerDisabled = true; if (workerInstance) { try { workerInstance.terminate(); } catch (_) { // ignore } } workerInstance = null; } function ensureWorker() { if (workerDisabled) return null; if (workerInstance) return workerInstance; if (typeof Worker === "undefined") return null; try { workerInstance = new Worker(new URL("./signatureVerifyWorker.js", import.meta.url), { type: "module", }); attachWorkerListeners(workerInstance); } catch (error) { devLogger.warn("[signatureVerifyWorkerClient] Failed to create worker", error); workerInstance = null; } return workerInstance; } // Main-thread fallback used only when Workers are unavailable. Preserves the // security guarantee (still verifies) at the cost of main-thread time. async function verifyOnMainThread(events) { let tools = getCachedNostrTools(); if (!tools || typeof tools.verifyEvent !== "function") { try { // Bound the wait: if nostr-tools is slow/unable to load, we must not stall // the feed waiting for it — fail open below instead of hanging. tools = await Promise.race([ ensureNostrTools(), new Promise((resolve) => setTimeout(() => resolve(null), 2000)), ]); } catch (_) { tools = null; } } const valid = new Set(); if (!tools || typeof tools.verifyEvent !== "function") { // The verification infrastructure itself is unavailable (e.g. nostr-tools // failed to load in this context). Fail OPEN for the read feed: dropping // every event would blank the feed entirely, which is far worse than briefly // showing unverified events when the verifier is broken. The pool already // does not verify, so this does not regress the prior security posture. devLogger.warn( "[signatureVerifyWorkerClient] verifier unavailable; passing events through unverified", ); for (const event of events) { if (event?.id) valid.add(event.id); } return valid; } for (const event of events) { try { if (!event || !event.id) continue; if (typeof tools.validateEvent === "function" && !tools.validateEvent(event)) continue; if (tools.verifyEvent(event) === true) valid.add(event.id); } catch (_) { // skip } } return valid; } /** * Verify a batch of events. Resolves with a Set of valid event ids. * @param {Array} events * @param {{ timeoutMs?: number }} [options] * @returns {Promise>} */ export function verifyEventsInWorker(events, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { const list = Array.isArray(events) ? events.filter((e) => e && e.id) : []; if (!list.length) return Promise.resolve(new Set()); const worker = ensureWorker(); if (!worker) { return verifyOnMainThread(list); } const id = ++requestId; return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { pending.delete(id); // The worker hung (e.g. couldn't load nostr-tools). Stop using it so we // don't pay this timeout on every subsequent batch, then fall back. disableWorker(); reject(new Error("verify-worker-timeout")); }, timeoutMs); pending.set(id, { resolve, reject, timeoutId }); try { worker.postMessage({ id, events: list }); } catch (error) { clearTimeout(timeoutId); pending.delete(id); reject(error instanceof Error ? error : new Error("verify-worker-post-failed")); } }).catch(async (error) => { // On worker failure/timeout, fall back to main-thread verification rather // than dropping events or trusting them blindly. devLogger.warn("[signatureVerifyWorkerClient] Falling back to main-thread verify:", error?.message || error); return verifyOnMainThread(list); }); } // Test/diagnostic helpers. export const __signatureVerifyInternals = { verifyOnMainThread };