diff --git a/eslint.config.js b/eslint.config.js index 5404d7f..1c8b866 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,6 +25,7 @@ export default [ AbortController: "readonly", requestAnimationFrame: "readonly", setTimeout: "readonly", + Response: "readonly", }, }, rules: { diff --git a/src/page_assets/index/js/main.js b/src/page_assets/index/js/main.js index 3841777..883e5d2 100644 --- a/src/page_assets/index/js/main.js +++ b/src/page_assets/index/js/main.js @@ -4,5 +4,22 @@ import '../../../scss/styles.scss'; import { init } from '../../../common/js/scl-app'; +async function registerCoiServiceWorker() { + if (!('serviceWorker' in navigator)) return; + if (window.crossOriginIsolated) return; + + try { + await navigator.serviceWorker.register('/sw-coi.js', { scope: '/' }); + await navigator.serviceWorker.ready; + if (!window.crossOriginIsolated) { + window.location.reload(); + } + } catch { + // Ignore registration failures; app can still run without SW header shim. + } +} + // Start the app -init(); +registerCoiServiceWorker().then(() => { + init(); +}); diff --git a/src/public/sw-coi.js b/src/public/sw-coi.js new file mode 100644 index 0000000..93ba8cc --- /dev/null +++ b/src/public/sw-coi.js @@ -0,0 +1,58 @@ +/* + Best-effort COI shim: adds missing COOP/COEP/CORP headers to same-origin GET responses. + Note: This only applies after the service worker controls the page. +*/ + +const REQUIRED_HEADERS = { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Resource-Policy': 'same-origin', +}; + +self.addEventListener('install', (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + + // Only patch same-origin GET requests we can safely proxy. + if (request.method !== 'GET') return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + event.respondWith( + (async () => { + let networkResponse; + try { + networkResponse = await fetch(request); + } catch (error) { + // If the network request fails (e.g. offline), just return the error response. + return new Response(error.message, { status: 503, statusText: 'Service Unavailable' }); + } + + // Do not attempt to rewrite opaque or redirected responses. + if (networkResponse.type === 'opaque' || networkResponse.type === 'opaqueredirect') { + return networkResponse; + } + + const headers = new self.Headers(networkResponse.headers); + Object.entries(REQUIRED_HEADERS).forEach(([name, value]) => { + if (!headers.has(name)) { + headers.set(name, value); + } + }); + + return new self.Response(networkResponse.body, { + status: networkResponse.status, + statusText: networkResponse.statusText, + headers, + }); + })() + ); +});