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