Files
ssmithxandClaude Opus 5.5 3215da07ed feat: installable FIPS-aware player for any podcast RSS feed
The player now takes any RSS feed URL as well as a podsteadr server,
and plays podsteadr feeds hosted on the FIPS mesh.

A browser can't speak FIPS, and an HTTPS page can't fetch or play
http:// URLs, so .fips URLs are routed one of two ways (src/lib/net.ts):
- served over the mesh (http://<npub>.fips:8097/), the page fetches
  .fips URLs directly over the device's own FIPS connection;
- served over HTTPS (player.atobitcoin.io), .fips URLs are rewritten to
  the host's /fips/<host>/<port>/<path> gateway, which relays only
  podsteadr feeds (8095) and blossom blobs (8098) over the host's FIPS
  link (deploy/player-domain.conf).

Also:
- "Add" accepts a feed, an OPML catalog, or a server address and works
  out which; old saved servers migrate to the new subscription format
- PWA: manifest, icons, service worker caching only the app shell
  (never feeds, audio or the gateway), install button, safe areas
- lock-screen controls and artwork via the Media Session API
- artwork, dates and durations in the episode list
- no secure-context-only APIs on the mesh copy (randomUUID, clipboard)
- buying episodes over FIPS is refused with a clear message: NIP-98
  signs the server's public URL, which a .fips address never matches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:16:38 +00:00

62 lines
1.9 KiB
JavaScript

// Offline app shell for the installed PWA. Only the player's own files are cached:
// feeds must stay fresh, and audio (range requests, often hundreds of MB) and the
// /fips/ gateway always go straight to the network.
const CACHE = 'podsteadr-player-v1';
const SHELL = ['./', 'manifest.webmanifest', 'icons/icon.svg', 'icons/icon-192.png', 'icons/icon-512.png'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)).then(() => self.skipWaiting()));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim()),
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
const scope = new URL(self.registration.scope);
if (
request.method !== 'GET' ||
url.origin !== scope.origin ||
url.pathname.startsWith(`${scope.pathname}fips/`) ||
request.headers.has('range')
) {
return;
}
// Page loads: network first so a deploy shows up immediately; cached shell when offline.
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((res) => {
const copy = res.clone();
caches.open(CACHE).then((cache) => cache.put('./', copy));
return res;
})
.catch(() => caches.match('./')),
);
return;
}
// Hashed build assets and icons never change under the same URL: cache first.
event.respondWith(
caches.match(request).then(
(hit) =>
hit ||
fetch(request).then((res) => {
if (res.ok) {
const copy = res.clone();
caches.open(CACHE).then((cache) => cache.put(request, copy));
}
return res;
}),
),
);
});