From e220f46d3525447a1547017d4f4933c3ec4f028c Mon Sep 17 00:00:00 2001 From: platform-mcp Date: Wed, 24 Jun 2026 01:45:37 +0530 Subject: [PATCH] agent: Host WRNexus push service worker at site root --- public/wrnexus-push-sw.js | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 public/wrnexus-push-sw.js diff --git a/public/wrnexus-push-sw.js b/public/wrnexus-push-sw.js new file mode 100644 index 0000000..78c55a3 --- /dev/null +++ b/public/wrnexus-push-sw.js @@ -0,0 +1,166 @@ +/*! + * WRNexus Web Push — service worker. + * + * ┌─────────────────────────────────────────────────────────────────────────┐ + * │ HOST THIS FILE ON YOUR OWN ORIGIN. │ + * │ │ + * │ A service worker can only control the origin it is served from, so the │ + * │ browser will NOT let wrnexus.com register a worker for your site. Copy │ + * │ this file to the ROOT of your site and serve it at: │ + * │ │ + * │ https://your-domain.example/wrnexus-push-sw.js │ + * │ │ + * │ It must be served same-origin, over HTTPS, with │ + * │ `Content-Type: text/javascript`. To control the whole site from a │ + * │ non-root path, also send `Service-Worker-Allowed: /`. │ + * └─────────────────────────────────────────────────────────────────────────┘ + * + * Core behaviour needs NO configuration — the click/delivery tracking URLs are + * carried inside each push payload built by the WRNexus API. The optional + * CONFIG block below is only used to silently re-subscribe after the browser + * rotates a subscription (`pushsubscriptionchange`); leave it blank to skip + * that. + */ + +/* eslint-disable no-restricted-globals */ + +// ── OPTIONAL: only needed for automatic re-subscription on key rotation ────── +var WRNEXUS_PUSH_CONFIG = { + // Your site's publicKey (the same value as data-site-id in the snippet). + siteKey: '', + // The WRNexus API origin (e.g. 'https://api.wrnexus.com'). + apiBase: '', + // Your site's VAPID public key (shown in Push → Settings). + vapidPublicKey: '', +}; + +self.addEventListener('install', function () { + self.skipWaiting(); +}); + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()); +}); + +// CORS-simple POST (text/plain, no custom headers, no credentials) so the +// receipt reaches the WRNexus API cross-origin without a preflight. +function postReceipt(url) { + if (!url) return Promise.resolve(); + return fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'omit', + keepalive: true, + mode: 'cors', + }).catch(function () {}); +} + +function urlBase64ToUint8Array(base64String) { + var padding = '='.repeat((4 - (base64String.length % 4)) % 4); + var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); + var raw = self.atob(base64); + var out = new Uint8Array(raw.length); + for (var i = 0; i < raw.length; ++i) out[i] = raw.charCodeAt(i); + return out; +} + +function keyToB64(buf) { + if (!buf) return ''; + var bytes = new Uint8Array(buf); + var binary = ''; + for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + return self + .btoa(binary) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} + +// ── Incoming notification ───────────────────────────────────────────────── +self.addEventListener('push', function (event) { + var data = {}; + try { + data = event.data ? event.data.json() : {}; + } catch (_e) { + try { + data = { title: event.data && event.data.text() }; + } catch (_e2) { + data = {}; + } + } + + var title = data.title || 'Notification'; + var options = { + body: data.body || '', + icon: data.icon || undefined, + image: data.image || undefined, + badge: data.badge || undefined, + tag: data.tag || data.campaignId || undefined, + requireInteraction: !!data.requireInteraction, + data: { + clickUrl: data.clickUrl || data.url || '/', + url: data.url || '/', + openUrl: data.openUrl || '', + campaignId: data.campaignId || '', + subscriptionId: data.subscriptionId || '', + }, + }; + + event.waitUntil( + Promise.all([ + self.registration.showNotification(title, options), + // Delivery receipt — records a `delivered` event for analytics. + postReceipt(data.deliveredUrl), + ]) + ); +}); + +// ── Click → record + open the destination ───────────────────────────────── +self.addEventListener('notificationclick', function (event) { + var d = (event.notification && event.notification.data) || {}; + event.notification.close(); + + // `clickUrl` is a signed WRNexus redirect: hitting it records the `clicked` + // event and 302s to the real destination, so tracking fires from the open. + var target = d.clickUrl || d.url || '/'; + + event.waitUntil( + self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) { + // Reuse an existing tab on the destination when possible. + for (var i = 0; i < list.length; i++) { + var client = list[i]; + if (d.url && client.url === d.url && 'focus' in client) return client.focus(); + } + if (self.clients.openWindow) return self.clients.openWindow(target); + return undefined; + }) + ); +}); + +// ── Optional: re-subscribe after the browser rotates the subscription ────── +self.addEventListener('pushsubscriptionchange', function (event) { + var cfg = WRNEXUS_PUSH_CONFIG || {}; + if (!cfg.siteKey || !cfg.apiBase || !cfg.vapidPublicKey) return; + event.waitUntil( + self.registration.pushManager + .subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(cfg.vapidPublicKey) }) + .then(function (sub) { + return fetch(cfg.apiBase.replace(/\/+$/, '') + '/api/push/subscriptions', { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'omit', + mode: 'cors', + body: JSON.stringify({ + key: cfg.siteKey, + endpoint: sub.endpoint, + keys: { + p256dh: keyToB64(sub.getKey('p256dh')), + auth: keyToB64(sub.getKey('auth')), + }, + metadata: { source: 'pushsubscriptionchange' }, + }), + }).catch(function () {}); + }) + .catch(function () {}) + ); +});