1/* ========================================================== 2 SERVICE WORKER â Mobile Safe & Integrity Compatible 3 (fetch handler corrected) 4 ========================================================== */ 5 6// ð¸ Bump this when you deploy updates 7const CACHE_VERSION = 'v7'; 8const CACHE_NAME = `npcdn-cache-${CACHE_VERSION}`; 9const OFFLINE_URL = 'ex_offline.html'; 10 11/* ======================== 12 ð§± INSTALL EVENT 13 ======================== */ 14self.addEventListener('install', (event) => { 15 console.log('[SW] Installing version', CACHE_VERSION); 16 17 event.waitUntil( 18 (async () => { 19 const cache = await caches.open(CACHE_NAME); 20 try { 21 await cache.add(new Request(OFFLINE_URL, { cache: 'reload' })); 22 } catch (e) { 23 console.warn('[SW] Could not cache offline page:', e); 24 } 25 })() 26 ); 27 28 self.skipWaiting(); 29}); 30 31/* ======================== 32 ð ACTIVATE EVENT 33 ======================== */ 34self.addEventListener('activate', (event) => { 35 console.log('[SW] Activating and cleaning old caches...'); 36 37 event.waitUntil( 38 (async () => { 39 const keys = await caches.keys(); 40 await Promise.all( 41 keys.map((key) => { 42 if (key !== CACHE_NAME) { 43 console.log('[SW] Deleting old cache:', key); 44 return caches.delete(key); 45 } 46 }) 47 ); 48 49 if ('navigationPreload' in self.registration) { 50 await self.registration.navigationPreload.enable(); 51 } 52 53 await self.clients.claim(); 54 })() 55 ); 56}); 57 58/* ======================== 59 ð FETCH EVENT (corrected) 60 ======================== 61 THE FIX: 62 - HTML navigations: network-first with offline fallback (unchanged behaviour). 63 - CSS / JS / images / fonts: NOT intercepted. We let the browser fetch them 64 natively. That preserves Subresource Integrity, uses the normal HTTP cache 65 (so most assets on refresh come from disk and never hit the network or the 66 WAF), and removes the `cache:'reload'` burst that was forcing every asset 67 through the "Checking your browser" challenge on shared-IP networks (Maxis). 68 ======================== */ 69self.addEventListener('fetch', (event) => { 70 const req = event.request; 71 72 // Only touch GET navigations. Everything else falls through 73 // to the browser's default handling (no event.respondWith). 74 if (req.method !== 'GET') return; 75 76 if (req.mode === 'navigate') { 77 event.respondWith( 78 (async () => { 79 try { 80 const preloadResponse = await event.preloadResponse; 81 if (preloadResponse) return preloadResponse; 82 return await fetch(req); 83 } catch (error) { 84 console.warn('[SW] Navigation fetch failed; serving offline page.', error); 85 const cache = await caches.open(CACHE_NAME); 86 return (await cache.match(OFFLINE_URL)) || Response.error(); 87 } 88 })() 89 ); 90 return; 91 } 92 93 // No branch for CSS/JS/images: intentionally left to the browser. 94}); 95 96/* ======================== 97 ð PUSH NOTIFICATION HANDLING 98 ======================== */ 99self.addEventListener('push', (event) => { 100 if (!(self.Notification && self.Notification.permission === 'granted')) return; 101 102 const sendNotification = (payload) => { 103 const title = payload.title || 'Notification'; 104 const body = payload.body || ''; 105 const icon = payload.icon || '/icon.png'; 106 const data = { url: payload.url || '/' }; 107 108 return self.registration.showNotification(title, { 109 body, 110 icon, 111 data, 112 }); 113 }; 114 115 if (event.data) { 116 const payload = event.data.json(); 117 event.waitUntil(sendNotification(payload)); 118 } 119}); 120 121/* ======================== 122 ð PUSH SUBSCRIPTION CHANGE 123 ======================== */ 124self.addEventListener('pushsubscriptionchange', (event) => { 125 event.waitUntil( 126 self.registration.pushManager 127 .subscribe({ 128 userVisibleOnly: true, 129 applicationServerKey: urlB64ToUint8Array( 130 'BHjtiK75djCrpfhTHanqvpvjvyfk6f2g_uPQHTSQgdI9EtiKIwIHfrVQXuT63RaeQSWun5L0ggcrWIbI3axDUv8' 131 ), 132 }) 133 .then((newSub) => { 134 self.clients.matchAll().then((clients) => {
135 clients.forEach((client) => 136 client.postMessage({ type: 'new-subscription', subscription: newSub }) 137 ); 138 }); 139 }) 140 .catch((err) => console.error('[SW] Failed to resubscribe:', err)) 141 ); 142}); 143 144/* ======================== 145 ð±ï¸ NOTIFICATION CLICK 146 ======================== */ 147self.addEventListener('notificationclick', (event) => { 148 event.notification.close(); 149 const targetUrl = event.notification.data.url; 150 151 event.waitUntil( 152 clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => { 153 for (const client of windowClients) { 154 if (client.url === targetUrl && 'focus' in client) { 155 return client.focus(); 156 } 157 } 158 return clients.openWindow(targetUrl); 159 }) 160 ); 161}); 162 163/* ======================== 164 ð§¹ MANUAL CACHE CLEAR MESSAGE 165 ======================== */ 166self.addEventListener('message', (event) => { 167 if (event.data && event.data.type === 'CLEAR_ALL_CACHE') { 168 caches.keys().then((keys) => keys.forEach((k) => caches.delete(k))); 169 console.log('[SW] Manual cache clear requested.'); 170 } 171}); 172 173/* ======================== 174 âï¸ HELPER: VAPID key conversion 175 ======================== */ 176function urlB64ToUint8Array(base64String) { 177 const padding = '='.repeat((4 - (base64String.length % 4)) % 4); 178 const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); 179 const rawData = atob(base64); 180 const output = new Uint8Array(rawData.length); 181 for (let i = 0; i < rawData.length; ++i) output[i] = rawData.charCodeAt(i); 182 return output; 183}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.