mls-bridge.mjs raw

   1  // MLS Bridge — routes MLS commands between shell SW and signer extension.
   2  // Loaded by index.html. Does not require TinyGo recompilation.
   3  
   4  // Relay URLs from the most recent mls.init — used for publish/subscribe routing.
   5  let _mlsRelays = [];
   6  
   7  // --- Fix 1b: Queue MLS_PROXY messages until window.nostr.mls is available ---
   8  let _proxyQueue = [];
   9  let _proxyReady = false;
  10  let _proxyPoll = null;
  11  
  12  function checkProxyReady() {
  13    if (window.nostr?.mls) {
  14      _proxyReady = true;
  15      if (_proxyPoll) { clearInterval(_proxyPoll); _proxyPoll = null; }
  16      // Drain queued messages.
  17      const q = _proxyQueue.splice(0);
  18      for (const d of q) handleMlsProxy(d);
  19    }
  20  }
  21  
  22  function handleMlsProxy(d) {
  23    if (!_proxyReady) {
  24      _proxyQueue.push(d);
  25      if (!_proxyPoll) _proxyPoll = setInterval(checkProxyReady, 200);
  26      return;
  27    }
  28    const method = d[1];
  29    (async () => {
  30      try {
  31        switch (method) {
  32          case 'init': {
  33            _mlsRelays = d[2] || [];
  34            const lastTS = parseInt(localStorage.getItem('marmot_last_event_ts') || '0', 10) || 0;
  35            await window.nostr.mls.init(d[2], lastTS);
  36            break;
  37          }
  38          case 'sendDM':
  39            await window.nostr.mls.sendDM(d[2], d[3]);
  40            break;
  41          case 'subscribe':
  42            await window.nostr.mls.subscribe();
  43            break;
  44          case 'publishKP':
  45            await window.nostr.mls.publishKP();
  46            break;
  47          case 'listGroups': {
  48            const groups = await window.nostr.mls.listGroups();
  49            postToSW('["MLS_GROUPS",' + JSON.stringify(groups) + ']');
  50            break;
  51          }
  52          case 'deliverEvent':
  53            await window.nostr.mls.deliverEvent(d[2], d[3]);
  54            break;
  55          case 'backupGroups':
  56            await window.nostr.mls.backupGroups();
  57            break;
  58          case 'restoreGroups':
  59            await window.nostr.mls.restoreGroups();
  60            break;
  61          case 'ratchetGroup':
  62            await window.nostr.mls.ratchetGroup(d[2]);
  63            break;
  64        }
  65      } catch (err) {
  66        console.error('mls-bridge: ' + method + ' failed:', err);
  67      }
  68    })();
  69  }
  70  
  71  // Handle MLS_PROXY messages from the shell SW.
  72  if (navigator.serviceWorker) {
  73    navigator.serviceWorker.addEventListener('message', async (event) => {
  74      const d = event.data;
  75      if (!Array.isArray(d) || d[0] !== 'MLS_PROXY') return;
  76      handleMlsProxy(d);
  77    });
  78  }
  79  
  80  // Handle MLS push events from the signer extension.
  81  // These are dispatched as 'nostr-mls' CustomEvents by the injected script.
  82  window.addEventListener('nostr-mls', (event) => {
  83    const data = event.detail;
  84    if (!data) return;
  85    switch (data.cmd) {
  86      case 'publish':
  87        postToSW('["MLS_PUBLISH",' + JSON.stringify(data.event) + ',' + JSON.stringify(_mlsRelays) + ']');
  88        break;
  89      case 'subscribe':
  90        postToSW('["MLS_SUBSCRIBE",' + JSON.stringify(String(data.subId)) + ',' + data.filter + ',' + JSON.stringify(_mlsRelays) + ']');
  91        break;
  92      case 'dm': {
  93        const rec = JSON.stringify({
  94          peer: data.peer, sender: data.sender, content: data.content,
  95          ts: data.ts, source: data.source, eventId: data.eventId
  96        });
  97        postToSW('["MLS_DM",' + rec + ']');
  98        break;
  99      }
 100      case 'status': {
 101        const msg = data.msg;
 102        postToSW('["MLS_STATUS",' + JSON.stringify(msg) + ']');
 103        // Ratchet completion — clear DM history for the peer.
 104        if (typeof msg === 'string' && msg.startsWith('ratchet ok:')) {
 105          const peer = msg.slice('ratchet ok:'.length);
 106          postToSW('["CLEAR_DM_HISTORY",' + JSON.stringify(peer) + ']');
 107        }
 108        break;
 109      }
 110      case 'relays':
 111        _mlsRelays = data.relays || [];
 112        break;
 113      case 'mls_ts':
 114        if (data.ts > 0) localStorage.setItem('marmot_last_event_ts', String(data.ts));
 115        break;
 116    }
 117  });
 118  
 119  // --- Fix 1a: Check for relay URLs that were set before this script loaded ---
 120  if (window._nostrMlsRelays && window._nostrMlsRelays.length > 0) {
 121    _mlsRelays = window._nostrMlsRelays;
 122  }
 123  
 124  let _mlsQueue = null;
 125  function postToSW(msg) {
 126    const sw = navigator.serviceWorker;
 127    if (!sw) return;
 128    if (sw.controller) {
 129      sw.controller.postMessage(msg);
 130    } else {
 131      if (!_mlsQueue) {
 132        _mlsQueue = [];
 133        // Fix 1e: removed { once: true } — second controller swap must be detected.
 134        sw.addEventListener('controllerchange', () => {
 135          if (sw.controller && _mlsQueue) {
 136            for (const m of _mlsQueue) sw.controller.postMessage(m);
 137          }
 138          _mlsQueue = null;
 139        });
 140      }
 141      _mlsQueue.push(msg);
 142    }
 143  }
 144