onlyn00bs-badge

OnlyN00bs: a DEF CON 34 friend-finder badge. ESP32 firmware, Web Bluetooth setup app, printable case
git clone https://git.virtualshack.io/onlyn00bs-badge.git
Log | Files | Refs | README | LICENSE

sw.js (3760B)


      1 /* badge.virtualshack.io — offline shell
      2  *
      3  * Why this exists: DEF CON's network is hostile and congested, and the setup app is
      4  * useless if you can't load it at the moment you need it.
      5  * Load the site once before you travel and it keeps working after.
      6  *
      7  * Strategy: precache the whole (tiny) site on install, then serve cache-first and
      8  * refresh in the background. Cache-first is deliberate — on a bad network, "instant
      9  * and slightly stale" beats "spinner forever".
     10  *
     11  * Bump CACHE on every content change, otherwise returning visitors keep the old copy
     12  * until the background refresh lands (i.e. one reload behind).
     13  *
     14  * Bump the ?v= on the stylesheet <link> in index.html to the SAME number at the same time.
     15  * index.html is served no-cache but /assets/ is not, so a CSS edit otherwise sits behind
     16  * Cloudflare's edge cache for hours after a deploy. Keeping the two numbers equal means
     17  * there is one version to remember, not two.
     18  */
     19 
     20 const CACHE = "onlyn00bs-v39";
     21 
     22 // Resolved against the worker's own location, so it's right whether the site is served at
     23 // / (production) or under a path (a local preview).
     24 const GALLERY_IMG = new URL("assets/gallery/", self.location).pathname;
     25 
     26 const PRECACHE = [
     27   "./",
     28   "index.html",
     29   "assets/badge.css",
     30   "assets/icon.svg",
     31   "manifest.webmanifest",
     32   // The setup app. Precached deliberately: this is THE page you need working on a hostile
     33   // con network, and it is self-contained (no imports, no external assets), so one entry
     34   // covers the whole flow. A --public build omits it and the cache.add 404s harmlessly
     35   // (see allSettled below).
     36   "provision/",
     37 ];
     38 
     39 self.addEventListener("install", (e) => {
     40   e.waitUntil((async () => {
     41     const cache = await caches.open(CACHE);
     42     // Individually, not addAll(): one 404 shouldn't fail the whole install and
     43     // leave a recipient with no offline copy at all.
     44     await Promise.allSettled(PRECACHE.map((url) => cache.add(new Request(url, { cache: "reload" }))));
     45     await self.skipWaiting();
     46   })());
     47 });
     48 
     49 self.addEventListener("activate", (e) => {
     50   e.waitUntil((async () => {
     51     const keys = await caches.keys();
     52     await Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)));
     53     await self.clients.claim();
     54   })());
     55 });
     56 
     57 self.addEventListener("fetch", (e) => {
     58   const req = e.request;
     59   if (req.method !== "GET") return;
     60   const url = new URL(req.url);
     61   if (url.origin !== self.location.origin) return;   // never touch cross-origin
     62   // Gallery photos are network-only. Everything else this worker sees gets cached as it's
     63   // fetched, which is right for a ~2 MB site and wrong for a photo gallery: every photo
     64   // anyone ever opened would sit in their cache until the next CACHE bump. The gallery
     65   // page itself is still cached like any other page; only the images pass through.
     66   if (url.pathname.startsWith(GALLERY_IMG)) return;
     67 
     68   e.respondWith((async () => {
     69     const cache = await caches.open(CACHE);
     70     const hit = await cache.match(req, { ignoreSearch: true });
     71 
     72     const fresh = fetch(req)
     73       .then((res) => {
     74         if (res && res.ok && res.type === "basic") cache.put(req, res.clone());
     75         return res;
     76       })
     77       .catch(() => null);
     78 
     79     if (hit) return hit;                       // cache-first
     80 
     81     const res = await fresh;
     82     if (res) return res;
     83 
     84     // Offline and never cached: hand navigations the shell rather than a browser error.
     85     if (req.mode === "navigate") {
     86       const shell = await cache.match("./", { ignoreSearch: true }) || await cache.match("index.html");
     87       if (shell) return shell;
     88     }
     89     return new Response("offline — this page was never cached", {
     90       status: 503,
     91       headers: { "Content-Type": "text/plain" },
     92     });
     93   })());
     94 });