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