git / moonbite-network / static/wallet-sw.js · raw

/* Bump on every wallet release: activate() deletes caches whose name does not   match, so changing this string is what flushes stale assets from installed   PWAs. */const CACHE_VERSION = 'moonbite-wallet-v7-nocache-api';const CACHE_URLS = [  '/wallet',  '/static/wallet-pwa.html',  '/wallet-manifest.json',  'https://moonbite.org/api/blockchain/info',];// Install event - cache essential filesself.addEventListener('install', event => {  event.waitUntil(    caches.open(CACHE_VERSION).then(cache => {      return cache.addAll(CACHE_URLS).catch(err => {        console.log('Cache add failed:', err);      });    })  );  self.skipWaiting();});// Activate event - clean up old cachesself.addEventListener('activate', event => {  event.waitUntil(    caches.keys().then(cacheNames => {      return Promise.all(        cacheNames.map(cacheName => {          if (cacheName !== CACHE_VERSION) {            return caches.delete(cacheName);          }        })      );    })  );  self.clients.claim();});// API paths that return per-address or per-transaction data. Everything// under /api/chain/ (address, utxos, history), explorer tx/address lookups,// and any legacy wallet/address endpoints.const SENSITIVE_API = [  '/api/chain/',  '/api/explorer/tx',  '/api/explorer/address',  '/api/wallet/',  '/api/address/',  '/api/utxo',  '/api/balance',  '/api/tx',  '/api/transaction',];function isSensitiveApi(pathname) {  return SENSITIVE_API.some(p => pathname.startsWith(p));}// Fetch event - serve from cache, fallback to networkself.addEventListener('fetch', event => {  const { request } = event;  const url = new URL(request.url);  // Skip non-GET requests  if (request.method !== 'GET') {    return;  }  // Wallet data (address balances, UTXO sets, transaction lookups) must never  // be written to the Cache Storage on disk: it would leave a plaintext record  // of which addresses this device watches. Network-only, no fallback.  if (isSensitiveApi(url.pathname)) {    event.respondWith(fetch(request).catch(() => new Response(      JSON.stringify({ error: 'offline' }),      { headers: { 'Content-Type': 'application/json' }, status: 503 }    )));    return;  }  // Other API calls (chain-wide info), try network first, fallback to cache  if (url.pathname.startsWith('/api/')) {    event.respondWith(      fetch(request)        .then(response => {          // Cache successful API responses          if (response.ok) {            const cache = caches.open(CACHE_VERSION);            cache.then(c => c.put(request, response.clone()));          }          return response;        })        .catch(() => {          // Return cached API response if network fails          return caches.match(request).then(cached => {            return cached || new Response(              JSON.stringify({ error: 'offline' }),              { headers: { 'Content-Type': 'application/json' }, status: 503 }            );          });        })    );    return;  }  // Navigations and HTML: NETWORK FIRST.  //  // Cache-first here meant an installed wallet kept serving the first HTML it  // ever cached, so no deploy - including a security fix - could ever reach  // an existing user. Always try the network for the document and fall back to  // cache only when genuinely offline, which keeps the PWA usable on a plane  // without freezing its code forever.  const wantsHTML = request.mode === 'navigate' ||    (request.headers.get('accept') || '').includes('text/html');  if (wantsHTML) {    event.respondWith(      fetch(request, { cache: 'no-store' })        .then(response => {          if (response.ok) {            const copy = response.clone();            caches.open(CACHE_VERSION).then(cache => cache.put(request, copy));          }          return response;        })        .catch(() => caches.match(request).then(          cached => cached || caches.match('/wallet')        ))    );    return;  }  // Static assets, esp. the wallet's JS crypto modules: NETWORK FIRST.  //  // Cache-first froze the code — a returning wallet kept running whatever  // modules it first cached, so a fix to key derivation or signing could  // never reach it. For a wallet that is unacceptable. Always try the network  // and fall back to cache only when offline.  event.respondWith(    fetch(request, { cache: 'no-store' })      .then(response => {        if (response.ok) {          const copy = response.clone();          caches.open(CACHE_VERSION).then(cache => cache.put(request, copy));        }        return response;      })      .catch(() => caches.match(request))  );});// Background sync for pending transactionsself.addEventListener('sync', event => {  if (event.tag === 'sync-wallet') {    event.waitUntil(syncWallet());  }});async function syncWallet() {  try {    const pending = await getPendingTransactions();    for (const tx of pending) {      await submitTransaction(tx);    }  } catch (err) {    console.error('Sync failed:', err);  }}async function getPendingTransactions() {  const db = await openDB();  // Implementation would depend on IndexedDB setup  return [];}async function submitTransaction(tx) {  // Implementation for submitting pending transactions  return true;}async function openDB() {  return new Promise((resolve, reject) => {    const request = indexedDB.open('MoonBiteWallet', 1);    request.onerror = () => reject(request.error);    request.onsuccess = () => resolve(request.result);  });}// Message handler for client communicationself.addEventListener('message', event => {  if (event.data && event.data.type === 'SKIP_WAITING') {    self.skipWaiting();  }  if (event.data && event.data.type === 'GET_CACHE_STATUS') {    caches.keys().then(cacheNames => {      event.ports[0].postMessage({        type: 'CACHE_STATUS',        caches: cacheNames      });    });  }});// Periodic background sync for wallet updates (if supported)self.addEventListener('periodicsync', event => {  if (event.tag === 'update-wallet') {    event.waitUntil(updateWalletData());  }});async function updateWalletData() {  try {    const response = await fetch('/api/blockchain/info');    if (response.ok) {      const clients = await self.clients.matchAll();      clients.forEach(client => {        client.postMessage({          type: 'WALLET_UPDATE',          data: response        });      });    }  } catch (err) {    console.error('Wallet update failed:', err);  }}