signer-wasm-host.mjs raw
1 // signer-wasm-host.mjs — Worker bootstrap for signer.wasm in page-embedded mode.
2 //
3 // Runs as a dedicated Worker (not a browser extension). The supervisor
4 // (wasm-host.mjs) routes signer method calls in the form { id, method, params }
5 // and receives { id, result } responses back.
6 //
7 // Bridge differences from ext mode:
8 // ext_storage_* → localStorage via supervisor storage-proxy messages
9 // ext_is_in_page → always 1 (enables in-page vault session path)
10 // ext_session_* → in-memory Map (volatile, same as ext mode)
11 // No ext_console_log, ext_send_message_to_tab, ext_get_active_tab
12
13 import {
14 makeCoreHelpers, makeWasi, makeCommonBridge,
15 computeBuildHash,
16 } from './bridge-common.mjs';
17
18 let mem, xp;
19
20 const _sabTable = new Map();
21 const _sabSeqRef = { value: 0 };
22 const _spawnedWorkerChans = new Map();
23 const _wasmUrlRef = { value: null };
24 const _buildHashRef = { value: null };
25
26 const _storageCbs = new Map(); // msgId -> callback (localStorage proxy)
27 const _sessionCbs = new Map(); // msgId -> callback (sessionStorage proxy)
28 let _storageSeq = 0;
29 let _extMsgCbID = -1; // cbID registered by signer WASM via ext_on_message
30
31 const h = makeCoreHelpers(() => mem, () => xp);
32 const { readStr, readBytes, writeStr, writeI32, writeBytes, cb0, cbs, cbb, cbdata } = h;
33
34 function jsonEsc(s) {
35 return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
36 .replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
37 }
38
39 const bridge = {
40 // --- localStorage via supervisor storage-proxy ---
41 ext_storage_get(kPtr, kLen, kCap, cbID) {
42 const msgId = _storageSeq++;
43 _storageCbs.set(msgId, val => cbs(cbID, val));
44 self.postMessage({ type: 'sig-storage-get', msgId, key: readStr(kPtr, kLen) });
45 },
46 ext_storage_set(kPtr, kLen, kCap, vPtr, vLen, vCap) {
47 self.postMessage({ type: 'sig-storage-set', key: readStr(kPtr, kLen), value: readStr(vPtr, vLen) });
48 },
49 ext_storage_remove(kPtr, kLen, kCap) {
50 self.postMessage({ type: 'sig-storage-remove', key: readStr(kPtr, kLen) });
51 },
52
53 // --- in-page flag and session storage ---
54 ext_is_in_page() { return 1; },
55 // session storage proxied to supervisor (main thread has sessionStorage)
56 ext_session_get(kPtr, kLen, kCap, cbID) {
57 const msgId = _storageSeq++;
58 _sessionCbs.set(msgId, val => cbs(cbID, val));
59 self.postMessage({ type: 'sig-session-get', msgId, key: readStr(kPtr, kLen) });
60 },
61 ext_session_set(kPtr, kLen, kCap, vPtr, vLen, vCap) {
62 self.postMessage({ type: 'sig-session-set', key: readStr(kPtr, kLen), value: readStr(vPtr, vLen) });
63 },
64
65 // --- message routing (same format as extension) ---
66 ext_on_message(cbID) { _extMsgCbID = cbID; },
67 ext_on_message_respond(reqID, ptr, len, cap) {
68 self.postMessage({ type: 'sig-resp', id: reqID, result: readStr(ptr, len) });
69 },
70
71 // Called by the WASM once loadVault's StorageGet callback has populated
72 // vaultRawCache/vaultExists. Only then is the signer safe to answer
73 // getVaultStatus, unlockVault, etc.
74 signer_signal_ready() {
75 self.postMessage({ type: 'sig-ready' });
76 },
77
78 // --- common (channel ops, spawn, subtle_random_bytes) ---
79 ...makeCommonBridge(h, _sabTable, _sabSeqRef, _wasmUrlRef, _buildHashRef, _spawnedWorkerChans),
80 };
81
82 const wasi = makeWasi(() => mem);
83
84 self.addEventListener('unhandledrejection', function(ev) {
85 self.postMessage({ type: 'sig-error', message: 'unhandledrejection: ' + String(ev.reason) });
86 });
87
88 self.onmessage = async function(e) {
89 const d = e.data;
90
91 // localStorage proxy response from supervisor
92 if (d.type === 'sig-storage-result') {
93 const fn = _storageCbs.get(d.msgId);
94 if (fn) { _storageCbs.delete(d.msgId); fn(d.value || ''); }
95 return;
96 }
97 // sessionStorage proxy response from supervisor
98 if (d.type === 'sig-session-result') {
99 const fn = _sessionCbs.get(d.msgId);
100 if (fn) { _sessionCbs.delete(d.msgId); fn(d.value || ''); }
101 return;
102 }
103
104 // Boot
105 if (d.type === 'init' && d.mode === 'root') {
106 try {
107 const wasmBytes = await fetch(d.wasmUrl, { cache: 'no-store' }).then(r => r.arrayBuffer());
108 const buildHash = await computeBuildHash(wasmBytes);
109 _wasmUrlRef.value = d.wasmUrl;
110 _buildHashRef.value = buildHash;
111
112 const { instance } = await WebAssembly.instantiate(wasmBytes,
113 { bridge, wasi_snapshot_preview1: wasi });
114 mem = instance.exports.memory;
115 xp = instance.exports;
116 // Watchdog: if signer_signal_ready() doesn't fire within 5s (e.g. WASM
117 // panics before loadVault completes), force ready so the UI surfaces
118 // the failure via subsequent RPCs rather than hanging forever.
119 let _readySent = false;
120 const _origPost = self.postMessage.bind(self);
121 const _wrappedPost = function(msg) {
122 if (msg && msg.type === 'sig-ready') {
123 if (_readySent) return;
124 _readySent = true;
125 }
126 _origPost(msg);
127 };
128 self.postMessage = _wrappedPost;
129 setTimeout(function() {
130 if (!_readySent) {
131 console.warn('[signer-worker] watchdog: signer_signal_ready not called in 5s, forcing ready');
132 self.postMessage({ type: 'sig-ready' });
133 }
134 }, 5000);
135 xp._start();
136 // sig-ready is sent by the WASM via signer_signal_ready() once
137 // loadVault's StorageGet callback returns.
138 } catch (err) {
139 self.postMessage({ type: 'sig-error', message: 'boot: ' + String(err) });
140 }
141 return;
142 }
143
144 // Method call from supervisor: { id, method, params }
145 if (d.method !== undefined && _extMsgCbID >= 0 && xp) {
146 const paramsJSON = typeof d.params === 'string'
147 ? d.params
148 : JSON.stringify(d.params || {});
149 const [mPtr, mLen] = writeStr(d.method);
150 const [pPtr, pLen] = writeStr(paramsJSON);
151 try { xp.__hook_ext_on_message(d.id, mPtr, mLen, pPtr, pLen, 0); } catch (err) {
152 console.error('[signer] TRAP in', d.method, ':', String(err));
153 self.postMessage({ type: 'sig-error', message: '__hook_ext_on_message: ' + String(err) });
154 }
155 }
156 };
157