mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 16:32:28 +00:00
The UX harness (fake nip-07 extension) revealed that the off-thread verification could blank the feed: if nostr-tools is slow/unable to load inside the verify worker, the worker hangs, each batch waits the full 10s timeout, and the main-thread fallback could return an empty set (fail-closed) — dropping every event so no videos ever render, silently (devLogger is suppressed in prod). Harden it so verification can never stall or blank the read feed: - Worker timeout 10s -> 2.5s, and disable the worker for the session after one timeout/error so we never pay the timeout on every batch. - Bound the main-thread nostr-tools wait (Promise.race ~2s) so a slow/failed load can't hang the fallback. - Fail OPEN when the verifier is genuinely unavailable: pass events through unverified rather than dropping them (the pool already does not verify, so this does not regress the prior posture). Real verification still runs whenever nostr-tools is available. Adds a regression test (hung worker -> resolves via fallback, worker disabled). Also updates the harness to seed mode:"live" videos + a NIP-65 relay list for mock-relay isolation. Co-Authored-By: Claude Opus 4.8 <[email protected]>
166 lines
5.7 KiB
JavaScript
166 lines
5.7 KiB
JavaScript
// 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<object>} events
|
|
* @param {{ timeoutMs?: number }} [options]
|
|
* @returns {Promise<Set<string>>}
|
|
*/
|
|
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 };
|