Files
simplex-chat/apps/simplex-badge-service/web/public/sw.js
T
19e70faeec badges: webapp feature branch (#7548)
* badges: webapp (#7433)

* badges: service migrations, store and catalog

* badges: BTCPay provider and settlement poller

* badges: web listener and /api endpoints

* web: checkout single-page app

* badges: tests and BTCPay fixtures

* badges: README and ini reference

* badges: fix hex16 build on GHC 8.10.7

* badges: Stripe card lane

* badges: fix Stripe card checkout, add theming

* badges: add a discount row to the order summary

* badges: site navbar, embedding, theme, Forget move

* badges: use SB code prefix in web checkout

* badges: rename sxb app namespace to sb

* badges: embed checkout nav via site; keep original app navbar

* badges: post iframe height, apply site background when embedded

* badges: embed dark surfaces, steadier iframe height

* badges: hide app footer when embedded

* badges: size embedded body to content, not viewport

* badges: declare color-scheme to stop reload flash

* badges: fade shell in on load, no reload blank

* badges: prerender app shell into index.html

* badges: pre-paint theme, hide shell on deep reload

* badges: logo returns to landing client-side

* badges: embedded wizard back, buy-a-code, resume

* badges: signal app-managed screens, resume across reload

* badges: rebuild wizard history on deep load so Back walks it

* badges: carry welcome-page height as the iframe floor

* badges: keep selection on Buy a code; rename to Your codes

* badges: read web shell as UTF-8, not locale

* badges: resume the exact paid order after Stripe card redirect

* badges: move docker deploy under scripts

* badges: add serve_webapp toggle and webapp export

* badges: wire split webapp deploy in docker config

* badges: quiet agent logs by default

* badges: resume card redirect in the embedded frame

* badges: migrate Stripe adapter to PaymentIntents

* badges: correct Stripe restricted key scopes in ini example

* badges: card via Payment Element and PaymentIntents

* badges: fix stale Checkout Session wording in Stripe adapter

* badges: fix stale CheckoutActions reference in card comment

* badges: order shell stylesheet before bootstrap script

* badges: remove development card stand-in

* badges: theme the Stripe card form with the site palette

* badges: exclude web from the Haskell build stage

* badges: unify invoice cancel and mark canceled

* badges: default log level to info

* badges: unify closed-invoice buy-again button

* badges: mute agent connection logs at info level

* badges: show purchase time in local timezone in Your codes

* badges: log service events on own channel, quiet agent

* badges: fold service migrations into one baseline

* badges: run compose on postgres over host network

* badges: use high-res hero art

* badges: add web CI to catch stale builds

* badges: rebuild web shell from committed source

* badges: normalize invoice-code link and columns

* badges: drop unused columns, rename index

* badges: note deferred receipt_hash in migrations

* badges: apply code-review fixes

* badges: reduce comments across service and web

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* badges: improve web page (#7546)

* badges: improve web page

* improve layout

* improve layout

* fix

* small changes

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* badges: read one issuer key from the ini

* badges: move and group the service tests

* badges: service fixes (#7567)

* badges: match the redeem error wording in tests

* badges: drop unused imports in the bot tests

* badges: cancel Stripe orders when they expire

* badges: correct the Stripe config and docs

* badges: refuse to revoke a redeemed code

* badges: make the fake Stripe cancel like Stripe

* badges: limit replayed webhook deliveries

---------

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: shum <github.shum@liber.li>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2026-09-25 09:01:51 +00:00

156 lines
7.4 KiB
JavaScript

// A classic worker script, so its export is the worker's own global: `self`. `test/sw.test.ts`
// runs this exact file in a Node `vm` with a fake Cache API and reads `self.sw`.
/** Rewritten by `build.js` from the bytes of the compiled modules, the stylesheet and the images. */
const BUILD = "9539a668abb6a8c8";
const ASSETS = `/assets/${BUILD}/`;
/** One cache per build, so eviction is "everything that is not this one". */
const CACHE = `sb-${BUILD}`;
const SHELL = "/";
// past this the cached shell is served instead; an answer that arrives later is dropped
const SHELL_TIMEOUT_MS = 3000;
/** Proves a document really is this build's shell and not an interstitial. */
const ENTRY = `${ASSETS}main.js`;
// Explicit URLs under one build hash, so a shell and the modules it imports can never skew.
// `/` is not here: it is fetched and checked separately, being the one response an
// interstitial can substitute. The images and fonts are precached because the stylesheet asks
// for them by relative URL, which puts them under this hash too.
const PRECACHE = [
`${ASSETS}styles.css`,
`${ASSETS}init.js`,
`${ASSETS}hero-dark.png`,
`${ASSETS}hero-light.png`,
`${ASSETS}wordmark-dark.svg`,
`${ASSETS}wordmark-light.svg`,
`${ASSETS}symbol-dark.svg`,
`${ASSETS}symbol-light.svg`,
`${ASSETS}GT-Walsheim-LC-Bold.woff2`,
`${ASSETS}Manrope-Regular.woff2`,
`${ASSETS}Manrope-Medium.woff2`,
`${ASSETS}Manrope-SemiBold.woff2`,
`${ASSETS}Manrope-Bold.woff2`,
`${ASSETS}api.js`,
`${ASSETS}order.js`,
`${ASSETS}format.js`,
`${ASSETS}domain.js`,
`${ASSETS}embed.js`,
`${ASSETS}catalog.js`,
`${ASSETS}codes.js`,
`${ASSETS}crowdfunding.js`,
`${ASSETS}flow.js`,
`${ASSETS}icons.js`,
`${ASSETS}main.js`,
`${ASSETS}parse.js`,
`${ASSETS}qr.js`,
`${ASSETS}routing.js`,
`${ASSETS}screens.js`,
`${ASSETS}store.js`,
// our card module, not Stripe's script, which is another origin and never cached
`${ASSETS}stripe.js`,
];
/** The whole routing decision, as a pure function of the URL. */
function strategyFor(url) {
if (url.origin !== self.location.origin) return "bypass";
// Before anything that could reach a cache. A cached `paid` would show a code for an order
// that later failed, and a cached `open` would hide a settled one; `Cache-Control: no-store`
// covers the HTTP cache, and this line covers the Cache API.
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) return "network-only";
if (url.pathname === SHELL || url.pathname === "/index.html") return "shell";
if (url.pathname.startsWith(ASSETS)) return "asset";
return "bypass";
}
/** Leaves the cache either complete or absent: a half-filled one serves a shell whose modules
* 404 offline, for as long as that build lives. */
async function precache() {
// This file is not part of its own hash, so a change to it alone re-installs against the
// same cache name and `caches.open` hands back the one the active worker is serving from.
// What this install created is all it may destroy.
const existed = (await caches.keys()).includes(CACHE);
const cache = await caches.open(CACHE);
try {
// Anubis serves its challenge as HTML at the page's own path, so the shell is verified
// before it is stored. `reload` so the answer is not one the HTTP cache had lying about.
const shell = await fetch(SHELL, { cache: "reload", credentials: "same-origin" });
if (!shell.ok) throw new Error(`sw: the shell answered ${shell.status}`);
const html = await shell.clone().text();
if (!html.includes(ENTRY)) throw new Error("sw: refusing to cache a document that is not this build's shell");
await cache.put(SHELL, shell);
await cache.addAll(PRECACHE);
} catch (e) {
// A failed re-install must leave the shipped build alone: offline support is what a buyer
// on a train has left, and an update check meeting a 5xx is no reason to take it away.
if (!existed) await caches.delete(CACHE);
throw e;
}
}
self.addEventListener("install", (event) => {
// After the precache, never before: a worker that took over with a half-filled cache would
// serve a shell whose modules are not there. Without `skipWaiting` at all, a new build only
// activated once every tab holding the old worker had closed, and a reload does not release
// one, so plain reloads kept getting the old build until a hard reload.
//
// `clients.claim()` stays out: it would swap the worker under a page whose modules are
// already loaded, which is the skew this whole file exists to prevent.
event.waitUntil(precache().then(() => self.skipWaiting()));
});
self.addEventListener("activate", (event) => {
// Safe even with an old page still open: every asset URL carries its build hash, so a
// page whose cache has gone falls through to the network rather than to another build's file.
event.waitUntil(caches.keys().then((names) => Promise.all(
names.filter((name) => name !== CACHE).map((name) => caches.delete(name)),
)));
});
/** Cache-first, safe because an asset URL carries the build hash, so an entry can never be
* another build's file. Nothing is written here: this worker caches one build, once. */
async function fromPrecache(request) {
const cache = await caches.open(CACHE);
const hit = await cache.match(request);
return hit !== undefined && hit !== null ? hit : fetch(request);
}
/** Network-first, and the one response that must be: the shell is the only file whose URL
* carries no build hash, so it decides which build a page runs. Served cache-first, a redeploy
* could not reach a returning buyer at all, which once shipped a checkout generating codes
* the service could not redeem. */
async function shellFirst(request) {
let deadline;
try {
// SHELL rather than the request: every page URL is the same document, so one fetch serves
// them all and it is the key the fallback is stored under. `reload` because the HTTP cache
// can pin a build just as the Cache API can.
const network = fetch(SHELL, { cache: "reload", credentials: "same-origin" });
// a captive portal or a tunnel accepts the connection and never answers, and `fetch` does
// not reject on that: without a deadline the page is blank for the browser's own timeout
const stall = new Promise((resolve) => { deadline = setTimeout(() => { resolve(undefined); }, SHELL_TIMEOUT_MS); });
const fresh = await Promise.race([network, stall]);
if (fresh !== undefined && fresh.ok) return fresh;
} catch {
// offline, or the listener is down: the precached shell is what is left
} finally {
// the offline path throws past a clear that followed the await, and left the worker awake
clearTimeout(deadline);
}
const cache = await caches.open(CACHE);
const hit = await cache.match(SHELL);
return hit !== undefined && hit !== null ? hit : fetch(request);
}
self.addEventListener("fetch", (event) => {
const request = event.request;
if (request.method !== "GET") return;
const strategy = strategyFor(new URL(request.url));
// Not responding is what keeps `/api/*` out of the Cache API: a catch-all handler would
// defeat `Cache-Control: no-store` on the way past.
if (strategy === "network-only" || strategy === "bypass") return;
// `?order=<id>` and `#/codes` are the same document, so the query string is not part of the key.
event.respondWith(strategy === "shell" ? shellFirst(request) : fromPrecache(request));
});
self.sw = { BUILD, CACHE, ASSETS, SHELL, ENTRY, PRECACHE, strategyFor };