1/* ========================================================================= 2 Encontra Push â Service Worker para sites estaticos. 3 4 Modelo: src/pages/encontra-push-sw.js.ts troca os marcadores __EP_*__ no 5 build e publica o resultado em /encontra-push-sw.js (scope "/"). 6 7 Mesmo comportamento do Service Worker do plugin WordPress, com uma 8 diferenca: os eventos vao direto para a API publica do painel, porque o 9 site estatico nao tem servidor para reencaminhar. Nenhuma credencial vive 10 aqui â so a chave publica do site, que ja esta no HTML. 11 ========================================================================= */ 12 13const EP_VERSION = '1.0.3'; 14const EP_API = 'https://push.encontra.com.br/api/v1/public/pk_04e0475db4c6061b4818c2fc587a5279'; // https://push.../api/v1/public/pk_... 15 16self.addEventListener('install', () => { 17 self.skipWaiting(); 18}); 19 20self.addEventListener('activate', (event) => { 21 event.waitUntil(self.clients.claim()); 22}); 23 24/* ----------------------------------------------------------------- push -- */ 25 26self.addEventListener('push', (event) => { 27 event.waitUntil(handlePush(event)); 28}); 29 30async function handlePush(event) { 31 const payload = readPayload(event); 32 33 // Toda mensagem precisa virar notificacao visivel: navegadores revogam a 34 // permissao de origens que recebem push "silencioso". 35 if (!payload) { 36 await self.registration.showNotification('Novidades', { 37 body: 'Toque para ver as novidades do site.', 38 tag: 'encontra-push-fallback', 39 }); 40 41 return; 42 } 43 44 const receivedBeacon = sendEvent('received', payload); 45 46 const options = { 47 body: payload.body || '', 48 icon: payload.icon || undefined, 49 badge: payload.badge || undefined, 50 image: payload.image || undefined, 51 tag: payload.tag || undefined, 52 renotify: Boolean(payload.renotify && payload.tag), 53 requireInteraction: Boolean(payload.requireInteraction), 54 timestamp: Date.now(), 55 data: { 56 url: payload.url, 57 message_id: payload.message_id, 58 campaign_id: payload.campaign_id, 59 event_token: payload.data && payload.data.event_token, 60 }, 61 }; 62 63 if (Array.isArray(payload.actions) && payload.actions.length) { 64 options.actions = payload.actions.slice(0, 2); 65 } 66 67 await self.registration.showNotification(payload.title || 'Novidades', options); 68 69 await Promise.all([receivedBeacon, sendEvent('shown', payload)]); 70} 71 72function readPayload(event) { 73 if (!event.data) { 74 return null; 75 } 76 77 try { 78 return event.data.json(); 79 } catch (e) { 80 try { 81 const text = event.data.text(); 82 83 return text ? { title: 'Novidades', body: text } : null; 84 } catch (err) { 85 return null; 86 } 87 } 88} 89 90/* ------------------------------------------------------ notificationclick */ 91 92self.addEventListener('notificationclick', (event) => { 93 event.notification.close(); 94 95 const data = event.notification.data || {}; 96 97 event.waitUntil( 98 Promise.all([ 99 sendEvent('clicked', { message_id: data.message_id, data: { event_token: data.event_token } }), 100 openTarget(data.url), 101 ]) 102 ); 103}); 104 105async function openTarget(url) { 106 if (!url) { 107 return; 108 } 109 110 let target; 111 112 try { 113 target = new URL(url); 114 } catch (e) { 115 return; 116 } 117 118 // So paginas web: nunca javascript:, data: ou file:. 119 if (target.protocol !== 'https:' && target.protocol !== 'http:') { 120 return; 121 } 122 123 const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); 124 125 for (const client of clientList) { 126 let current; 127 128 try { 129 current = new URL(client.url); 130 } catch (e) { 131 continue; 132 } 133 134 if (current.origin === target.origin && 'focus' in client) { 135 await client.focus(); 136 137 if ('navigate' in client && current.href !== target.href) { 138 return client.navigate(target.href); 139 } 140 141 return; 142 } 143 } 144 145 if (self.clients.openWindow) { 146 return self.clients.openWindow(target.href); 147 } 148} 149 150/* ------------------------------------------------------ notificationclose */ 151 152self.addEventListener('notificationclose', (event) => { 153 const data = event.notification.data || {}; 154 155 event.waitUntil( 156 sendEvent('closed', { message_id: data.message_id, data: { event_token: data.event_token } }) 157 ); 158}); 159 160/* ------------------------------------------------- pushsubscriptionchange */ 161 162self.addEventListener('pushsubscriptionchange', (event) => { 163 event.waitUntil(renewSubscription(event)); 164}); 165 166async function renewSubscription(event) { 167 try { 168 const oldSubscription = event.oldSubscription || (await self.registration.pushManager.getSubscription()); 169 170 const applicationServerKey = event.oldSubscription && event.oldSubscription.options 171 ? event.oldSubscription.options.applicationServerKey 172 : null; 173 174 if (!applicationServerKey) { 175 return; 176 } 177 178 const fresh = await self.registration.pushManager.subscribe({ 179 userVisibleOnly: true, 180 applicationServerKey: applicationServerKey, 181 }); 182 183 const json = fresh.toJSON(); 184 185 await post('/subscribe', { 186 endpoint: json.endpoint, 187 p256dh: json.keys && json.keys.p256dh, 188 auth: json.keys && json.keys.auth, 189 source_url: self.registration.scope, 190 }); 191 192 if (oldSubscription && oldSubscription.endpoint !== json.endpoint) { 193 await post('/unsubscribe', { endpoint: oldSubscription.endpoint }); 194 } 195 } catch (e) { 196 // O proximo envio falhara com erro permanente e o painel invalidara a 197 // subscription antiga sozinho. 198 } 199} 200 201/* ---------------------------------------------------------------- beacon -- */ 202 203/* 204 * JSON enviado como text/plain: e uma requisicao "simples" de CORS, sem 205 * preflight. O painel decodifica o corpo do mesmo jeito. 206 */ 207function post(path, body) { 208 return fetch(EP_API + path, { 209 method: 'POST', 210 mode: 'cors', 211 credentials: 'omit', 212 headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, 213 body: JSON.stringify(body), 214 keepalive: true, 215 }); 216} 217 218function sendEvent(type, payload) { 219 const token = payload && payload.data && payload.data.event_token; 220 221 if (!token) { 222 return Promise.resolve(); 223 } 224 225 return post('/events', { 226 events: [ 227 { 228 type: type, 229 token: token, 230 event_id: generateEventId(), 231 event_at: new Date().toISOString(), 232 }, 233 ], 234 }).catch(() => { 235 // Beacon e melhor esforco: perder uma metrica nunca impede a notificacao. 236 }); 237} 238
239/** ULID simplificado: ordenavel por tempo e sem colisao pratica. */ 240function generateEventId() { 241 const alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; 242 let timestamp = Date.now(); 243 let id = ''; 244 245 for (let i = 0; i < 10; i++) { 246 id = alphabet[timestamp % 32] + id; 247 timestamp = Math.floor(timestamp / 32); 248 } 249 250 const random = new Uint8Array(16); 251 252 if (self.crypto && self.crypto.getRandomValues) { 253 self.crypto.getRandomValues(random); 254 } 255 256 for (let i = 0; i < 16; i++) { 257 id += alphabet[random[i] % 32]; 258 } 259 260 return id; 261}
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.