sw.js raw

   1  // smesh2 service worker — thin orchestrator
   2  // delegates to crypto.js, db.js, pool.js, dm.js
   3  
   4  import { signEvent, schnorr, bytesToHex } from './crypto.js'
   5  import { saveEvent, queryEvents, getConversationList, queryDMs } from './db.js'
   6  import {
   7    setPoolState, setCallbacks, pool, subs,
   8    sendToRelay, handleProxy, handleEvent, handleRelayInfo,
   9  } from './pool.js'
  10  import {
  11    setDMState, processIncomingDM, sendDM, handleDMSub,
  12    handleExtDMResult, handleCryptoResult,
  13  } from './dm.js'
  14  
  15  const CACHE_NAME = 'smesh2-v52'
  16  const CACHE_URLS = ['./', './favicon.ico', './favicon.png', './favicon-96x96.png', './apple-touch-icon.png']
  17  
  18  // ─── module state ───────────────────────────────────────────────────
  19  
  20  let secretKey = null
  21  let secretKeyHex = null
  22  let myPubkey = null
  23  let writeRelays = []
  24  
  25  function syncState() {
  26    setPoolState({ writeRelays, secretKey, secretKeyHex, myPubkey })
  27    setDMState({ secretKey, secretKeyHex, myPubkey })
  28  }
  29  
  30  // ─── lifecycle ──────────────────────────────────────────────────────
  31  
  32  self.addEventListener('install', (e) => {
  33    e.waitUntil(
  34      caches.open(CACHE_NAME)
  35        .then((cache) => cache.addAll(CACHE_URLS))
  36        .catch((err) => console.warn('cache addAll failed, continuing:', err))
  37    )
  38    self.skipWaiting()
  39  })
  40  
  41  self.addEventListener('activate', (e) => {
  42    e.waitUntil(
  43      caches.keys().then((names) =>
  44        Promise.all(
  45          names
  46            .filter((n) => n !== CACHE_NAME)
  47            .map((n) => caches.delete(n))
  48        )
  49      )
  50    )
  51    self.clients.claim()
  52  })
  53  
  54  self.addEventListener('fetch', (e) => {
  55    const url = new URL(e.request.url)
  56    if (url.origin !== self.location.origin) return
  57    if (e.request.mode === 'navigate') {
  58      e.respondWith(
  59        fetch(e.request).catch(() => caches.match(e.request))
  60      )
  61      return
  62    }
  63    e.respondWith(
  64      caches.match(e.request).then((cached) => cached || fetch(e.request))
  65    )
  66  })
  67  
  68  // ─── subscriptions ──────────────────────────────────────────────────
  69  
  70  async function handleReq(clientId, subId, filter) {
  71    subs.set(subId, { filter, clientId })
  72    const events = await queryEvents(filter)
  73    const client = await self.clients.get(clientId)
  74    if (!client) return
  75    for (const ev of events) client.postMessage(['EVENT', subId, ev])
  76    client.postMessage(['EOSE', subId])
  77  }
  78  
  79  function handleClose(subId) {
  80    subs.delete(subId)
  81  }
  82  
  83  async function pushToMatchingSubs(event) {
  84    for (const [subId, { filter, clientId }] of subs) {
  85      if (matchesFilter(event, filter)) {
  86        const client = await self.clients.get(clientId)
  87        if (client) client.postMessage(['EVENT', subId, event])
  88      }
  89    }
  90  }
  91  
  92  function matchesFilter(ev, f) {
  93    if (f.ids && !f.ids.includes(ev.id)) return false
  94    if (f.authors && !f.authors.includes(ev.pubkey)) return false
  95    if (f.kinds && !f.kinds.includes(ev.kind)) return false
  96    if (f.since && ev.created_at < f.since) return false
  97    if (f.until && ev.created_at > f.until) return false
  98    for (const [k, v] of Object.entries(f)) {
  99      if (k.startsWith('#') && k.length === 2) {
 100        const tagName = k[1]
 101        if (!ev.tags?.some((t) => t[0] === tagName && v.includes(t[1]))) return false
 102      }
 103    }
 104    return true
 105  }
 106  
 107  // ─── identity broadcast ─────────────────────────────────────────────
 108  
 109  async function broadcastIdentity(clientId, pubkey, relayUrls) {
 110    const events = await queryEvents({ authors: [pubkey], kinds: [0, 3, 10002, 10050, 10051] })
 111    const byKind = new Map()
 112    for (const ev of events) {
 113      const prev = byKind.get(ev.kind)
 114      if (!prev || ev.created_at > prev.created_at) byKind.set(ev.kind, ev)
 115    }
 116  
 117    const relayEvent = byKind.get(10002)
 118    const userRelays = relayEvent
 119      ? relayEvent.tags.filter((t) => t[0] === 'r').map((t) => t[1])
 120      : writeRelays.length ? writeRelays : []
 121  
 122    if (!byKind.has(10050) && secretKey && userRelays.length) {
 123      const ev = signEvent({
 124        pubkey,
 125        created_at: Math.floor(Date.now() / 1000),
 126        kind: 10050,
 127        tags: userRelays.map((r) => ['relay', r]),
 128        content: ''
 129      }, secretKey)
 130      await saveEvent(ev)
 131      byKind.set(10050, ev)
 132    }
 133  
 134    if (!byKind.has(10051) && secretKey && userRelays.length) {
 135      const ev = signEvent({
 136        pubkey,
 137        created_at: Math.floor(Date.now() / 1000),
 138        kind: 10051,
 139        tags: userRelays.map((r) => ['relay', r]),
 140        content: ''
 141      }, secretKey)
 142      await saveEvent(ev)
 143      byKind.set(10051, ev)
 144    }
 145  
 146    const toSend = [...byKind.values()]
 147    if (!toSend.length) {
 148      const client = await self.clients.get(clientId)
 149      if (client) client.postMessage(['BROADCAST_DONE', 0, 0])
 150      return
 151    }
 152  
 153    for (const ev of toSend) {
 154      for (const url of relayUrls) sendToRelay(url, ['EVENT', ev])
 155    }
 156    const client = await self.clients.get(clientId)
 157    if (client) client.postMessage(['BROADCAST_DONE', toSend.length, relayUrls.length])
 158  }
 159  
 160  // ─── wire up pool callbacks ─────────────────────────────────────────
 161  
 162  async function broadcastToClients(msg) {
 163    const clients = await self.clients.matchAll()
 164    for (const client of clients) client.postMessage(msg)
 165  }
 166  
 167  setCallbacks({
 168    onEvent: pushToMatchingSubs,
 169    onDMEvent: processIncomingDM,
 170    broadcastToClients,
 171  })
 172  
 173  // ─── message dispatch ───────────────────────────────────────────────
 174  
 175  self.addEventListener('message', (e) => {
 176    const [type, ...args] = e.data
 177    const clientId = e.source?.id
 178  
 179    switch (type) {
 180      case 'REQ':        handleReq(clientId, args[0], args[1]); break
 181      case 'CLOSE':      handleClose(args[0]); break
 182      case 'EVENT':      handleEvent(clientId, args[0]); break
 183      case 'PROXY':      handleProxy(clientId, args[0], args[1], ...args.slice(2)); break
 184      case 'RELAY_INFO': handleRelayInfo(clientId, args[0]); break
 185      case 'SKIP_WAITING': self.skipWaiting(); break
 186  
 187      case 'SET_KEY': {
 188        secretKey = new Uint8Array(args[0])
 189        secretKeyHex = Array.from(secretKey, (b) => b.toString(16).padStart(2, '0')).join('')
 190        myPubkey = bytesToHex(schnorr.getPublicKey(secretKey))
 191        syncState()
 192        self.clients.get(clientId).then((c) => c?.postMessage(['KEY_SET']))
 193        break
 194      }
 195      case 'SET_PUBKEY': {
 196        myPubkey = args[0]
 197        syncState()
 198        break
 199      }
 200      case 'SIGN': {
 201        const [requestId, unsignedEvent] = args
 202        if (!secretKey) break
 203        try {
 204          const signed = signEvent(unsignedEvent, secretKey)
 205          self.clients.get(clientId).then((c) => {
 206            if (c) c.postMessage(['SIGNED', requestId, signed])
 207          })
 208        } catch (err) {
 209          self.clients.get(clientId).then((c) => {
 210            if (c) c.postMessage(['SIGN_ERROR', requestId, err.message])
 211          })
 212        }
 213        break
 214      }
 215      case 'CLEAR_KEY': {
 216        secretKey = null
 217        secretKeyHex = null
 218        myPubkey = null
 219        writeRelays = []
 220        syncState()
 221        break
 222      }
 223      case 'SET_WRITE_RELAYS': {
 224        writeRelays = args[0] || []
 225        syncState()
 226        break
 227      }
 228      case 'BROADCAST': {
 229        broadcastIdentity(clientId, args[0], args[1])
 230        break
 231      }
 232      case 'SEND_DM': {
 233        sendDM(clientId, args[0], args[1], args[2])
 234        break
 235      }
 236      case 'DM_EXT_RESULT': {
 237        const [peer, content, success, errMsg] = args
 238        handleExtDMResult(clientId, peer, content, success, errMsg)
 239        break
 240      }
 241      case 'DM_SUB': {
 242        handleDMSub(clientId, args[0])
 243        break
 244      }
 245      case 'DM_LIST': {
 246        getConversationList().then((list) => {
 247          self.clients.get(clientId).then((c) => {
 248            if (c) c.postMessage(['DM_LIST', list])
 249          })
 250        })
 251        break
 252      }
 253      case 'DM_HISTORY': {
 254        const [peer, limit, until] = args
 255        queryDMs(peer, limit || 50, until).then((messages) => {
 256          self.clients.get(clientId).then((c) => {
 257            if (c) c.postMessage(['DM_HISTORY', peer, messages])
 258          })
 259        })
 260        break
 261      }
 262      case 'CRYPTO_RESULT': {
 263        const [requestId, result, error] = args
 264        handleCryptoResult(requestId, result, error)
 265        break
 266      }
 267    }
 268  })
 269