Files
pondering-orb/web/sw.js
T
Diablo_Rain 0b761f684b harden: relay timeouts, DNS resolver reuse, cache hygiene + frontend polish
Server (relay/SSRF):
- Bound connect+TLS+request under CONNECT_TIMEOUT and body download under
  BODY_TIMEOUT so a stalling upstream can't pin a relay permit indefinitely
- Add inbound header_read_timeout to defeat slow-loris connections
- Allowlist relayed Content-Type (fall back to octet-stream) to block
  content-confusion from attacker-controlled stream origins
- Reuse one DNS resolver process-wide instead of rebuilding it per request
  (restores hickory's DNS cache; drops per-segment resolver setup)
- Probe cache: random per-process hash seed (DefaultHasher was deterministic)
  + bounded growth with expired-entry eviction
- Real Ctrl-C handler for a clean shutdown (honours the banner)
- Collapse the wrap_raw no-op alias into wrap

Frontend:
- Guide: visible focus ring tracking arrow-key navigation
- Player: buffering/tuning indicator while <video> is stalled
- prefers-contrast: more pass (lifts --ink-dim to meet WCAG AA)
- Service worker: date-stamped cache names so shell updates propagate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:20:06 -04:00

90 lines
3.3 KiB
JavaScript

// sw.js — service worker (upgrade #7).
//
// Two strategies:
// - app shell (html/css/js/hls.js/icons): cache-first → instant repeat loads, offline guide UI
// - iptv-org JSON proxied via the relay (…/relay?url=…/*.json): stale-while-revalidate
// → instant paint from cache, silent refresh in the background
//
// Live stream manifests + segments (the rest of /relay) and /probe, /metrics are
// NEVER cached — the relay marks them no-store and we bypass them here, so the SW
// is the sole freshness authority for metadata only.
// Bump these on every shell change so returning visitors actually receive the
// new assets — a static 'v1' name would serve stale JS/CSS indefinitely. The
// activate handler deletes any cache whose name isn't in this current pair.
const SHELL = 'orb-shell-20260615';
const DATA = 'orb-data-20260615';
const SHELL_ASSETS = [
'/', '/index.html', '/styles.css',
'/app.js', '/player.js', '/zapper.js', '/guide.js', '/ambient.js',
'/lexicon.js', '/memory.js', '/health.js', '/epg-worker.js',
'/lib/hls.min.js', '/lib/xmltv-parse.js',
'/providers/source-provider.js', '/providers/mock.js', '/providers/iptv-org.js',
'/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png',
];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(SHELL).then((c) => c.addAll(SHELL_ASSETS)).then(() => self.skipWaiting()));
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys()
.then((keys) => Promise.all(keys.filter((k) => ![SHELL, DATA].includes(k)).map((k) => caches.delete(k))))
.then(() => self.clients.claim()),
);
});
self.addEventListener('fetch', (e) => {
const req = e.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return; // same-origin only
if (url.pathname === '/probe' || url.pathname === '/metrics') return;
if (url.pathname === '/relay') {
const upstream = url.searchParams.get('url') || '';
// Only the JSON metadata is cacheable; stream manifests/segments are not.
if (/\.json(\?|$)/i.test(upstream)) {
e.respondWith(staleWhileRevalidate(req, DATA));
}
return; // streams/segments → straight to the relay, uncached
}
// App shell / static assets → cache-first.
if (SHELL_ASSETS.includes(url.pathname)
|| url.pathname.startsWith('/icons/')
|| url.pathname.startsWith('/providers/')
|| url.pathname.startsWith('/lib/')) {
e.respondWith(cacheFirst(req, SHELL));
}
});
async function cacheFirst(req, cacheName) {
const cache = await caches.open(cacheName);
const hit = await cache.match(req);
if (hit) return hit;
try {
const res = await fetch(req);
if (res.ok) cache.put(req, res.clone());
return res;
} catch {
if (req.mode === 'navigate') {
const idx = await cache.match('/index.html');
if (idx) return idx;
}
return new Response('offline', { status: 503, statusText: 'offline' });
}
}
async function staleWhileRevalidate(req, cacheName) {
const cache = await caches.open(cacheName);
const hit = await cache.match(req);
const network = fetch(req)
.then((res) => { if (res.ok) cache.put(req, res.clone()); return res; })
.catch(() => hit || new Response('offline', { status: 503 }));
return hit || network;
}