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, makeEnv, 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 const env = makeEnv(() => mem);
84
85 self.addEventListener('unhandledrejection', function(ev) {
86 self.postMessage({ type: 'sig-error', message: 'unhandledrejection: ' + String(ev.reason) });
87 });
88
89 self.onmessage = async function(e) {
90 const d = e.data;
91
92 // localStorage proxy response from supervisor
93 if (d.type === 'sig-storage-result') {
94 const fn = _storageCbs.get(d.msgId);
95 if (fn) { _storageCbs.delete(d.msgId); fn(d.value || ''); }
96 return;
97 }
98 // sessionStorage proxy response from supervisor
99 if (d.type === 'sig-session-result') {
100 const fn = _sessionCbs.get(d.msgId);
101 if (fn) { _sessionCbs.delete(d.msgId); fn(d.value || ''); }
102 return;
103 }
104
105 // Boot
106 if (d.type === 'init' && d.mode === 'root') {
107 try {
108 const wasmBytes = await fetch(d.wasmUrl, { cache: 'no-store' }).then(r => r.arrayBuffer());
109 const buildHash = await computeBuildHash(wasmBytes);
110 _wasmUrlRef.value = d.wasmUrl;
111 _buildHashRef.value = buildHash;
112
113 const { instance } = await WebAssembly.instantiate(wasmBytes,
114 { bridge, env, wasi_snapshot_preview1: wasi });
115 mem = instance.exports.memory;
116 xp = instance.exports;
117 // Watchdog: if signer_signal_ready() doesn't fire within 5s (e.g. WASM
118 // panics before loadVault completes), force ready so the UI surfaces
119 // the failure via subsequent RPCs rather than hanging forever.
120 let _readySent = false;
121 const _origPost = self.postMessage.bind(self);
122 const _wrappedPost = function(msg) {
123 if (msg && msg.type === 'sig-ready') {
124 if (_readySent) return;
125 _readySent = true;
126 }
127 _origPost(msg);
128 };
129 self.postMessage = _wrappedPost;
130 setTimeout(function() {
131 if (!_readySent) {
132 console.warn('[signer-worker] watchdog: signer_signal_ready not called in 5s, forcing ready');
133 self.postMessage({ type: 'sig-ready' });
134 }
135 }, 5000);
136 xp._start();
137 // sig-ready is sent by the WASM via signer_signal_ready() once
138 // loadVault's StorageGet callback returns.
139 } catch (err) {
140 self.postMessage({ type: 'sig-error', message: 'boot: ' + String(err) });
141 }
142 return;
143 }
144
145 // Method call from supervisor: { id, method, params }
146 if (d.method !== undefined && _extMsgCbID >= 0 && xp) {
147 const paramsJSON = typeof d.params === 'string'
148 ? d.params
149 : JSON.stringify(d.params || {});
150 const [mPtr, mLen] = writeStr(d.method);
151 const [pPtr, pLen] = writeStr(paramsJSON);
152 try { xp.__hook_ext_on_message(d.id, mPtr, mLen, pPtr, pLen, 0); } catch (err) {
153 console.error('[signer] TRAP in', d.method, ':', String(err));
154 self.postMessage({ type: 'sig-error', message: '__hook_ext_on_message: ' + String(err) });
155 }
156 }
157 };
158