PageSourceSearch

https://cscase.net/browser-push-worker.js

js cscase.net collected 2026-10-10 00:10:20 UTC 11,978 bytes, 378 lines download raw bytes

1const normalizeNotificationUrl = (value) => {
2  try {
3    const url = new URL(String(value || '/'), self.location.origin);
4    return url.origin === self.location.origin
5      ? url.href
6      : self.location.origin;
7  } catch {
8    return self.location.origin;
9  }
10};
11
12const getNotificationId = (value) => {
13  const notificationId = Number(value);
14  return Number.isSafeInteger(notificationId) && notificationId > 0
15    ? notificationId
16    : null;
17};
18
19const withNotificationId = (value, notificationId) => {
20  const url = new URL(value, self.location.origin);
21  url.searchParams.set('notificationId', String(notificationId));
22  return url.href;
23};
24
25const BROWSER_PUSH_ACTION_PARAM = 'browserPushAction';
26const MAX_BROWSER_PUSH_ACTIONS = 2;
27
28const getSupportedNotificationActionCount = () => {
29  if (typeof Notification === 'undefined') return 0;
30  const maxActions = Number(Notification.maxActions);
31  return Number.isInteger(maxActions) && maxActions >= 0
32    ? Math.min(maxActions, MAX_BROWSER_PUSH_ACTIONS)
33    : 0;
34};
35
36const normalizeNotificationTargetUrl = (value) => {
37  try {
38    const raw = String(value || '').trim();
39    if (!raw) return null;
40    const url = new URL(raw, self.location.origin);
41    return url.protocol === 'http:' || url.protocol === 'https:' ? url : null;
42  } catch {
43    return null;
44  }
45};
46
47const normalizeNotificationActions = (value) => {
48  if (!Array.isArray(value)) return [];
49
50  return value
51    .slice(0, MAX_BROWSER_PUSH_ACTIONS)
52    .flatMap((candidate, index) => {
53      if (!candidate || typeof candidate !== 'object') return [];
54      const title = String(candidate.title || '').trim();
55      const url = normalizeNotificationTargetUrl(candidate.url);
56      if (!title || !url) return [];
57
58      const promoCode = String(candidate.promoCode || '').trim();
59      const rawPromoTimeout = Number(candidate.promoCodeTimeOut);
60      const promoCodeTimeOut =
61        Number.isInteger(rawPromoTimeout) &&
62        rawPromoTimeout >= 1 &&
63        rawPromoTimeout <= 43_200
64          ? rawPromoTimeout
65          : null;
66      return [
67        {
68          action: `cta-${index}`,
69          promoCode,
70          promoCodeTimeOut,
71          title,
72          url: url.href,
73        },
74      ];
75    });
76};
77
78const withNotificationAction = (value, action, notificationId) => {
79  const clickUrl = new URL(value, self.location.origin);
80  clickUrl.searchParams.set(BROWSER_PUSH_ACTION_PARAM, action);
81  if (notificationId) {
82    clickUrl.searchParams.set('notificationId', String(notificationId));
83  }
84  return clickUrl.href;
85};
86
87const isSameOriginWindow = (client) => {
88  try {
89    return new URL(client.url).origin === self.location.origin;
90  } catch {
91    return false;
92  }
93};
94
95const IMAGE_PRELOAD_TIMEOUT_MS = 8_000;
96const normalizeNotificationImageUrl = (value) => {
97  if (!value) return undefined;
98
99  try {
100    const url = new URL(String(value), self.location.origin);
101    return url.protocol === 'http:' || url.protocol === 'https:'
102      ? url.href
103      : undefined;
104  } catch {
105    return undefined;
106  }
107};
108
109const getBrowserPushBrand = () => {
110  const hostname = new URL(self.location.origin).hostname.toLowerCase();
111  const isSkala =
112    hostname === 'skalaskins.com' || hostname.endsWith('.skalaskins.com');
113
114  return {
115    icon: new URL(
116      isSkala
117        ? '/images/skalaskins.com/favicon.svg'
118        : '/images/cscase.com/favicon.svg',
119      self.location.origin,
120    ).href,
121    title: isSkala ? 'SkalaSkins' : 'CSCASE',
122  };
123};
124
125const preloadNotificationImage = async (url) => {
126  const controller = new AbortController();
127  const timeout = setTimeout(
128    () => controller.abort(),
129    IMAGE_PRELOAD_TIMEOUT_MS,
130  );
131
132  try {
133    const isCrossOrigin =
134      new URL(url, self.location.origin).origin !== self.location.origin;
135    const response = await fetch(url, {
136      cache: 'force-cache',
137      mode: isCrossOrigin ? 'no-cors' : 'same-origin',
138      signal: controller.signal,
139    });
140    const isOpaqueResponse = response.type === 'opaque';
141    if (!response.ok && !isOpaqueResponse) {
142      throw new Error('Notification image request failed');
143    }
144
145    if (!isOpaqueResponse && typeof createImageBitmap === 'function') {
146      const blob = await response.blob();
147      const bitmap = await createImageBitmap(blob);
148      bitmap.close?.();
149    }
150  } finally {
151    clearTimeout(timeout);
152  }
153};
154
155const BROWSER_PUSH_WORKER_VERSION = 6;
156const VERIFICATION_ATTEMPT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
157const VERIFICATION_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
158const MAX_VERIFICATION_MESSAGE_LENGTH = 160;
159const hasControlCharacter = (value) =>
160  Array.from(value).some((character) => {
161    const codePoint = character.codePointAt(0) || 0;
162    return codePoint <= 31 || codePoint === 127;
163  });
164
165self.addEventListener('install', (event) => {
166  event.waitUntil(self.skipWaiting());
167});
168
169self.addEventListener('activate', (event) => {
170  event.waitUntil(self.clients.claim());
171});
172
173self.addEventListener('message', (event) => {
174  if (event.data?.type !== 'browserPushCapabilityRequest') return;
175
176  const response = {
177    type: 'browserPushCapability',
178    version: BROWSER_PUSH_WORKER_VERSION,
179  };
180  const responsePort = event.ports?.[0];
181  if (responsePort) responsePort.postMessage(response);
182  else event.source?.postMessage(response);
183});
184
185self.addEventListener('push', (event) => {
186  if (!event.data) return;
187
188  let payload;
189  try {
190    payload = event.data.json();
191  } catch {
192    payload = { body: event.data.text() };
193  }
194
195  const verificationAttemptId = String(
196    payload.verificationAttemptId || payload.attemptId || '',
197  );
198  const verificationChallenge = String(payload.challenge || '');
199  const verificationMessage = String(payload.verificationMessage || '').trim();
200  if (
201    payload.type === 'browserPushVerification' &&
202    VERIFICATION_ATTEMPT_PATTERN.test(verificationAttemptId) &&
203    VERIFICATION_CHALLENGE_PATTERN.test(verificationChallenge) &&
204    verificationMessage &&
205    verificationMessage.length <= MAX_VERIFICATION_MESSAGE_LENGTH &&
206    !hasControlCharacter(verificationMessage)
207  ) {
208    const brand = getBrowserPushBrand();
209    event.waitUntil(
210      Promise.all([
211        self.registration.showNotification(brand.title, {
212          body: verificationMessage,
213          data: {
214            type: 'browserPushVerification',
215            attemptId: verificationAttemptId,
216            url: self.location.origin,
217          },
218          icon: brand.icon,
219          tag: 'browser-push-verification',
220          renotify: false,
221        }),
222        self.clients
223          .matchAll({ type: 'window', includeUncontrolled: true })
224          .then((windows) => {
225            windows.forEach((client) => {
226              client.postMessage({
227                type: 'browserPushVerification',
228                attemptId: verificationAttemptId,
229                challenge: verificationChallenge,
230              });
231            });
232          }),
233      ]),
234    );
235    return;
236  }
237
238  event.waitUntil(
239    (async () => {
240      const brand = getBrowserPushBrand();
241      const image = normalizeNotificationImageUrl(payload.image);
242      const icon = normalizeNotificationImageUrl(brand.icon);
243      const badge =
244        normalizeNotificationImageUrl(payload.badge) ??
245        normalizeNotificationImageUrl(brand.icon);
246      const notificationTitle =
247        String(payload.title || '').trim() || brand.title;
248      const actions = normalizeNotificationActions(payload.actions);
249      const supportedActionCount = getSupportedNotificationActionCount();
250      const visibleActions = actions.slice(0, supportedActionCount);
251      const defaultAction =
252        supportedActionCount === 0 ? (actions[0]?.action ?? null) : null;
253      const body = String(payload.body || '').trim();
254      const visibleBody = defaultAction
255        ? [body, `→ ${actions[0].title}`].filter(Boolean).join('\n')
256        : body;
257      const mediaUrls = [...new Set([image, icon, badge].filter(Boolean))];
258      const loadedMediaUrls = new Set();
259
260      await Promise.all(
261        mediaUrls.map(async (url) => {
262          try {
263            await preloadNotificationImage(url);
264            loadedMediaUrls.add(url);
265          } catch {
266            // A broken image must not prevent delivery of the notification.
267          }
268        }),
269      );
270
271      await self.registration.showNotification(notificationTitle, {
272        actions: visibleActions.map(({ action, title }) => ({ action, title })),
273        badge: loadedMediaUrls.has(badge)
274          ? badge
275          : loadedMediaUrls.has(icon)
276            ? icon
277            : undefined,
278        body: visibleBody,
279        data: {
280          actions,
281          defaultAction,
282          notificationId: payload.notificationId ?? null,
283          url: normalizeNotificationUrl(payload.url),
284        },
285        icon: loadedMediaUrls.has(icon) ? icon : undefined,
286        image: loadedMediaUrls.has(image) ? image : undefined,
287        tag: payload.tag ? String(payload.tag) : undefined,
288      });
289    })(),
290  );
291});
292
293self.addEventListener('notificationclick', (event) => {
294  event.notification.close();
295  const notificationId = getNotificationId(
296    event.notification.data?.notificationId,
297  );
298  const selectedAction = Array.isArray(event.notification.data?.actions)
299    ? event.notification.data.actions.find(
300        (action) =>
301          action?.action ===
302          (event.action || event.notification.data?.defaultAction),
303      )
304    : null;
305
306  event.waitUntil(
307    self.clients
308      .matchAll({ type: 'window', includeUncontrolled: true })
309      .then((windows) => {
310        if (selectedAction) {
311          const actionUrl = withNotificationAction(
312            new URL('/', self.location.origin).href,
313            selectedAction.action,
314            notificationId,
315          );
316          const sameOriginWindows = windows.filter(isSameOriginWindow);
317          const target =
318            sameOriginWindows.find(
319              (client) => client.visibilityState === 'visible',
320            ) ?? sameOriginWindows[0];
321          const openActionWindow = () =>
322            self.clients
323              .openWindow(actionUrl)
324              .then((openedWindow) => openedWindow?.focus());
325
326          if (!target || typeof target.navigate !== 'function') {
327            return openActionWindow();
328          }
329          return target
330            .navigate(actionUrl)
331            .then((navigatedWindow) => (navigatedWindow ?? target).focus())
332            .catch(openActionWindow);
333        }
334
335        if (notificationId) {
336          const sameOriginWindows = windows.filter(isSameOriginWindow);
337          const target =
338            sameOriginWindows.find(
339              (client) => client.visibilityState === 'visible',
340            ) ?? sameOriginWindows[0];
341
342          if (target) {
343            const targetUrl = withNotificationId(target.url, notificationId);
344            if (new URL(target.url).href === targetUrl) {
345              return target.focus();
346            }
347
348            const openTargetWindow = () =>
349              self.clients
350                .openWindow(targetUrl)
351                .then((openedWindow) => openedWindow?.focus());
352            if (typeof target.navigate !== 'function') {
353              return openTargetWindow();
354            }
355            return target
356              .navigate(targetUrl)
357              .then((navigatedWindow) => (navigatedWindow ?? target).focus())
358              .catch(openTargetWindow);
359          }
360
361          const rootUrl = withNotificationId(
362            new URL('/', self.location.origin).href,
363            notificationId,
364          );
365          return self.clients.openWindow(rootUrl);
366        }
367
368        const fallbackUrl = normalizeNotificationUrl(
369          event.notification.data?.url,
370        );
371        const target = windows.find(
372          (client) => normalizeNotificationUrl(client.url) === fallbackUrl,
373        );
374        if (target) return target.focus();
375        return self.clients.openWindow(fallbackUrl);
376      }),
377  );
378});

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.