// Main-thread wrapper around the crypto Web Worker (Argon2id pwhash + // sealed user-key open). Each unlock attempt spawns a fresh one-shot worker // — workers are cheap and the pwhash is a one-time cost per login, so we // avoid the bookkeeping needed for a persistent request queue. // // Falls back to inline (main-thread) `openUserKey` when the `Worker` // constructor is unavailable (e.g. vitest's jsdom environment, strict CSPs). // The fallback path is identical in semantics to the worker path; the only // difference is whether it blocks the main thread. // // Why not a long-lived worker? The KDF cost dwarfs the spawn cost (~1-2 s // vs. a few ms), and PIN-unlock happens at most once per session. Keeping a // worker resident would also require a request-id correlation map which the // decrypt.worker uses (because per-message decrypts are high-volume). import { openUserKey } from '@chat-app/shared/crypto'; import type { OpenUserKeyInput, OpenUserKeyResult, } from '../workers/crypto.worker'; type WorkerResponse = | { ok: true; result: OpenUserKeyResult } | { ok: false; error: string }; export type { OpenUserKeyInput, OpenUserKeyResult }; export async function openUserKeyInWorker( input: OpenUserKeyInput, ): Promise { const worker = new Worker( new URL('../workers/crypto.worker.ts', import.meta.url), { type: 'module' }, ); try { return await new Promise((resolve, reject) => { worker.addEventListener('message', (ev: MessageEvent) => { const msg = ev.data; if (msg && msg.ok) resolve(msg.result); else reject(new Error(msg?.error ?? 'crypto worker returned malformed response')); }); worker.addEventListener('error', (ev: ErrorEvent) => { reject(new Error(ev.message || 'crypto worker error')); }); worker.postMessage({ op: 'openUserKey', input }); }); } finally { worker.terminate(); } } // Public entry point: route through the worker when possible, fall back to // the synchronous-on-main-thread path otherwise. Callers should prefer this // over importing `openUserKey` directly so we get the worker speedup // everywhere it's available. export async function openUserKeyMaybeWorker( input: OpenUserKeyInput, ): Promise { if (typeof Worker === 'undefined') { return openUserKey(input); } try { const result = await openUserKeyInWorker(input); return result.privateKey; } catch (err) { // If the worker spawn or message round-trip fails (e.g. CSP blocks // module workers in some packaging modes), fall back to inline so the // unlock still succeeds — just with a brief main-thread hitch. console.warn('[cryptoWorker] worker path failed, falling back to inline', err); return openUserKey(input); } }