tempest-react-sdk 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/openapi/escape.mjs +210 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
- package/bin/lib/openapi/generate.mjs +93 -17
- package/bin/lib/openapi/schema-to-zod.mjs +41 -4
- package/bin/tempest.mjs +184 -3
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +127 -55
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +72 -62
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +12 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +151 -87
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/neural.cjs +2 -0
- package/dist/neural.cjs.map +1 -0
- package/dist/neural.d.ts +110 -0
- package/dist/neural.js +48 -0
- package/dist/neural.js.map +1 -0
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +271 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +271 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-owner.cjs +2 -0
- package/dist/telemetry/app-error-owner.cjs.map +1 -0
- package/dist/telemetry/app-error-owner.js +18 -0
- package/dist/telemetry/app-error-owner.js.map +1 -0
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +979 -62
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +16 -13
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +118 -63
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +21 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +11 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
package/dist/sw/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/** Whether a cached response has aged past `maxAgeSeconds` (best-effort, via `Date`). */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const dateHeader = response.headers.get(\"date\");\n if (!dateHeader) return false;\n const age = (Date.now() - new Date(dateHeader).getTime()) / 1000;\n return age > maxAgeSeconds;\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n}\n\nasync function networkFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"network-timeout\")), timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\nasync function staleWhileRevalidate(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })\n .catch(() => undefined);\n\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n const response = await network;\n if (response) return response;\n if (cached) return cached;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(request: Request, route: RuntimeRoute): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength`. Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n // Suffix range: last N bytes.\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n // Cache/serve the FULL resource once (no Range), then slice it.\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route).then((response) => createPartialResponse(request, response)),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cache.js","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"tempest-api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\nimport { credentialHeaderSet } from \"./credential-headers\";\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n /**\n * Opt in to caching requests that carry a credential header (see\n * `credentialHeaders`), and responses marked `Cache-Control: private`.\n * Default `false`.\n *\n * Cache Storage is keyed by URL and outlives the session: without this\n * gate, a `GET /api/me` stored for one account was served to the next\n * account on the same browser whenever the network failed or exceeded\n * `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With\n * the default, credentialed requests bypass the route entirely and reach\n * the network as if no service worker were installed.\n *\n * When enabled, every stored entry gets `Vary` on each credential header the\n * request carried, so the Cache API only matches it for the same credential —\n * another account (or no account) misses instead of reading it. A token\n * rotated while online simply repopulates the entry. Pair it with\n * `clearSessionData()` on logout, which removes the entries themselves.\n * `Cache-Control: no-store` is never cached, opt-in or not.\n *\n * A cookie session is invisible here: the worker cannot see `Cookie`, and\n * gating on `request.credentials` would switch off caching for every\n * same-origin image and font. A cookie-authenticated API stays private only\n * through `Cache-Control: private`/`no-store` on its responses and\n * `clearSessionData()` on logout.\n */\n cacheAuthenticated?: boolean;\n /**\n * Header names that mark a request as credentialed. Default\n * `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,\n * `x-auth-token`, …) — the same list the background-sync queue strips.\n */\n credentialHeaders?: readonly string[];\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/**\n * Header stamped on every runtime-cache entry with the epoch milliseconds of the\n * write. `Date` alone is not enough: it is not a CORS-safelisted response\n * header, so for a cross-origin API the worker reads `null` and `maxAgeSeconds`\n * would never expire anything.\n */\nconst CACHED_AT_HEADER = \"x-tempest-cached-at\";\n\n/**\n * Whether a cached response has aged past `maxAgeSeconds`. Reads the\n * {@link CACHED_AT_HEADER} stamp written by the runtime cache, and falls back to\n * `Date` for entries written before the stamp existed.\n */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const stamp = Number(response.headers.get(CACHED_AT_HEADER));\n const dateHeader = response.headers.get(\"date\");\n const storedAt = stamp > 0 ? stamp : dateHeader ? new Date(dateHeader).getTime() : NaN;\n if (Number.isNaN(storedAt)) return false;\n return (Date.now() - storedAt) / 1000 > maxAgeSeconds;\n}\n\n/** The `Cache-Control` directives of a response, lower-cased. */\nfunction cacheControlDirectives(response: Response): string[] {\n return (response.headers.get(\"cache-control\") ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((directive) => directive.trim().split(\"=\")[0]);\n}\n\n/**\n * Whether a network response may be written to the route's cache: it must be\n * `ok`, never `no-store`, and `private` only under `cacheAuthenticated`.\n */\nfunction isStorable(response: Response, route: RuntimeRoute): boolean {\n if (!response.ok) return false;\n const directives = cacheControlDirectives(response);\n if (directives.includes(\"no-store\")) return false;\n return route.cacheAuthenticated === true || !directives.includes(\"private\");\n}\n\n/** The credential headers, lower-cased, that a request carries for its route. */\nfunction credentialsOn(request: Request, route: RuntimeRoute): string[] {\n return [...credentialHeaderSet(route.credentialHeaders)].filter((name) =>\n request.headers.has(name),\n );\n}\n\n/**\n * Write a response to the route's cache with the {@link CACHED_AT_HEADER} stamp\n * and, under `cacheAuthenticated`, `Vary` on each credential header the request\n * carried, so the entry only matches the credential that produced it. Skips\n * responses that {@link isStorable} rejects, then trims the cache to\n * `maxEntries`.\n */\nasync function store(\n cache: Cache,\n request: Request,\n response: Response,\n route: RuntimeRoute,\n): Promise<void> {\n if (!isStorable(response, route)) return;\n const headers = new Headers(response.headers);\n headers.set(CACHED_AT_HEADER, String(Date.now()));\n if (route.cacheAuthenticated) {\n const vary = headers.get(\"vary\");\n const varies = (vary ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((name) => name.trim());\n const missing = credentialsOn(request, route).filter((name) => !varies.includes(name));\n if (missing.length > 0 && !varies.includes(\"*\")) {\n headers.set(\"vary\", [vary, ...missing].filter(Boolean).join(\", \"));\n }\n }\n const copy = response.clone();\n await cache.put(\n request,\n new Response(copy.body, { status: copy.status, statusText: copy.statusText, headers }),\n );\n await trimCache(route.cacheName, route.maxEntries);\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n}\n\n/**\n * Network-first. When the timeout wins, the network request keeps running to\n * refresh the cache, and is handed to `extend` (`event.waitUntil`) so the\n * worker is not stopped mid-write.\n */\nasync function networkFirst(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => {\n extend(network.catch(() => undefined));\n reject(new Error(\"network-timeout\"));\n }, timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\n/**\n * Stale-while-revalidate. A fresh cached response is served at once, and the\n * revalidation is handed to `extend` (`event.waitUntil`): without it the\n * browser may stop the worker before the refreshed response is written.\n */\nasync function staleWhileRevalidate(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n await store(cache, request, response, route);\n return response;\n })\n .catch(() => undefined);\n\n const fresh = cached && !isExpired(cached, route.maxAgeSeconds) ? cached : undefined;\n if (fresh) {\n extend(network);\n return fresh;\n }\n const response = await network;\n if (response) return response;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route, extend);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route, extend);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the\n * route's `credentialHeaders`) is also left untouched unless its route sets\n * `cacheAuthenticated`, and a response with `Cache-Control: no-store`\n * (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`\n * applies to every read, including the offline fallback: an expired entry is a\n * miss, never a last resort. Under `rangeRequests`, the full resource is cached\n * once (the `Range` header stripped) and each range is sliced from it.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n if (!route.cacheAuthenticated && credentialsOn(request, route).length > 0) return;\n const extend = (promise: Promise<unknown>): void => event.waitUntil(promise);\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route, extend).then((response) =>\n createPartialResponse(request, response),\n ),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route, extend));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=[`authorization`,`proxy-authorization`,`cookie`,`x-api-key`,`x-auth-token`,`x-csrf-token`,`x-xsrf-token`];function t(t=e){return new Set(t.map(e=>e.toLowerCase()))}exports.DEFAULT_CREDENTIAL_HEADERS=e,exports.credentialHeaderSet=t;
|
|
2
|
+
//# sourceMappingURL=credential-headers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-headers.cjs","names":[],"sources":["../../src/sw/credential-headers.ts"],"sourcesContent":["/**\n * Header names treated as credentials by the service-worker helpers: never\n * persisted by `installBackgroundSync`, and a request carrying any of them\n * bypasses `installRuntimeCache` unless the route opts in. `Cookie` and\n * `Proxy-Authorization` are listed for completeness although the browser\n * forbids scripts from setting them — a cookie session is invisible to the\n * worker, which is why it depends on `Cache-Control` and `clearSessionData()`.\n */\nexport const DEFAULT_CREDENTIAL_HEADERS: readonly string[] = [\n \"authorization\",\n \"proxy-authorization\",\n \"cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n];\n\n/**\n * Lower-case a list of header names into a set for case-insensitive lookups.\n *\n * @param names - Header names, any case. Default {@link DEFAULT_CREDENTIAL_HEADERS}.\n * @returns The lower-cased set.\n */\nexport function credentialHeaderSet(\n names: readonly string[] = DEFAULT_CREDENTIAL_HEADERS,\n): ReadonlySet<string> {\n return new Set(names.map((name) => name.toLowerCase()));\n}\n"],"mappings":"AAQA,IAAa,EAAgD,CACzD,gBACA,sBACA,SACA,YACA,eACA,eACA,cACJ,EAQA,SAAgB,EACZ,EAA2B,EACR,CACnB,OAAO,IAAI,IAAI,EAAM,IAAK,GAAS,EAAK,YAAY,CAAC,CAAC,CAC1D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/sw/credential-headers.ts
|
|
2
|
+
var e = [
|
|
3
|
+
"authorization",
|
|
4
|
+
"proxy-authorization",
|
|
5
|
+
"cookie",
|
|
6
|
+
"x-api-key",
|
|
7
|
+
"x-auth-token",
|
|
8
|
+
"x-csrf-token",
|
|
9
|
+
"x-xsrf-token"
|
|
10
|
+
];
|
|
11
|
+
function t(t = e) {
|
|
12
|
+
return new Set(t.map((e) => e.toLowerCase()));
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { e as DEFAULT_CREDENTIAL_HEADERS, t as credentialHeaderSet };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=credential-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-headers.js","names":[],"sources":["../../src/sw/credential-headers.ts"],"sourcesContent":["/**\n * Header names treated as credentials by the service-worker helpers: never\n * persisted by `installBackgroundSync`, and a request carrying any of them\n * bypasses `installRuntimeCache` unless the route opts in. `Cookie` and\n * `Proxy-Authorization` are listed for completeness although the browser\n * forbids scripts from setting them — a cookie session is invisible to the\n * worker, which is why it depends on `Cache-Control` and `clearSessionData()`.\n */\nexport const DEFAULT_CREDENTIAL_HEADERS: readonly string[] = [\n \"authorization\",\n \"proxy-authorization\",\n \"cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n];\n\n/**\n * Lower-case a list of header names into a set for case-insensitive lookups.\n *\n * @param names - Header names, any case. Default {@link DEFAULT_CREDENTIAL_HEADERS}.\n * @returns The lower-cased set.\n */\nexport function credentialHeaderSet(\n names: readonly string[] = DEFAULT_CREDENTIAL_HEADERS,\n): ReadonlySet<string> {\n return new Set(names.map((name) => name.toLowerCase()));\n}\n"],"mappings":";AAQA,IAAa,IAAgD;CACzD;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;AAQA,SAAgB,EACZ,IAA2B,GACR;CACnB,OAAO,IAAI,IAAI,EAAM,KAAK,MAAS,EAAK,YAAY,CAAC,CAAC;AAC1D"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./background-sync.cjs"),t=require("./cache-inspect.cjs");function n(e){return!e.startsWith(`tempest-precache-`)}async function r(r={}){let[i,a]=await Promise.all([t.clearCaches(r.caches??n),e.clearBackgroundSyncQueue(r.queueName)]);return{caches:i,queueCleared:a}}exports.clearSessionData=r;
|
|
2
|
+
//# sourceMappingURL=session-data.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-data.cjs","names":[],"sources":["../../src/sw/session-data.ts"],"sourcesContent":["import { clearBackgroundSyncQueue } from \"./background-sync\";\nimport { clearCaches } from \"./cache-inspect\";\nimport type { CacheFilter } from \"./cache-inspect\";\n\n/** Options for {@link clearSessionData}. */\nexport interface ClearSessionDataOptions {\n /**\n * Which Cache Storage buckets hold session data. Default: every bucket except\n * the app-shell precache (`tempest-precache-*`), so the app still launches\n * offline after logout while no runtime entry — whatever its `cacheName` —\n * survives into the next session.\n */\n caches?: CacheFilter;\n /** The background-sync `queueName` to clear. Default `tempest-bg-sync`. */\n queueName?: string;\n}\n\n/** What {@link clearSessionData} removed. */\nexport interface ClearSessionDataResult {\n /** Names of the Cache Storage buckets deleted. */\n caches: string[];\n /** Whether the background-sync queue was emptied. */\n queueCleared: boolean;\n}\n\n/** Default cache filter: everything but the versioned app-shell precache. */\nfunction isSessionCache(name: string): boolean {\n return !name.startsWith(\"tempest-precache-\");\n}\n\n/**\n * Remove everything the service-worker helpers keep on behalf of the signed-in\n * user: the runtime caches filled by `installRuntimeCache` and the queue of\n * mutations waiting in `installBackgroundSync`. Call it on logout, from the page.\n *\n * Both stores outlive the session and are shared by every account that signs in\n * on the browser. Measured in Chromium 153 (11/10/2026), before this existed:\n * the documented `clearCaches(\"tempest-\")` deleted nothing because the recipe's\n * runtime cache was named `api`, the next account read the previous account's\n * `GET /api/me` offline, and a mutation queued by the previous account was\n * replayed with its stored bearer after logout. A prefix filter cannot know the\n * names a recipe chose, so the default here inverts it: every bucket goes except\n * the precache, which holds only the build's static assets.\n *\n * Cache Storage and IndexedDB are origin storage, readable from the window, so\n * this needs no message to the worker.\n *\n * @param options - Narrow the caches to delete, or name a custom queue.\n * @returns The caches deleted and whether the queue was cleared.\n *\n * @example\n * import { clearSessionData } from \"tempest-react-sdk/sw\";\n *\n * async function logout() {\n * await clearSessionData();\n * }\n */\nexport async function clearSessionData(\n options: ClearSessionDataOptions = {},\n): Promise<ClearSessionDataResult> {\n const [caches, queueCleared] = await Promise.all([\n clearCaches(options.caches ?? isSessionCache),\n clearBackgroundSyncQueue(options.queueName),\n ]);\n return { caches, queueCleared };\n}\n"],"mappings":"0EA0BA,SAAS,EAAe,EAAuB,CAC3C,MAAO,CAAC,EAAK,WAAW,mBAAmB,CAC/C,CA6BA,eAAsB,EAClB,EAAmC,CAAC,EACL,CAC/B,GAAM,CAAC,EAAQ,GAAgB,MAAM,QAAQ,IAAI,CAC7C,EAAA,YAAY,EAAQ,QAAU,CAAc,EAC5C,EAAA,yBAAyB,EAAQ,SAAS,CAC9C,CAAC,EACD,MAAO,CAAE,SAAQ,cAAa,CAClC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { clearBackgroundSyncQueue as e } from "./background-sync.js";
|
|
2
|
+
import { clearCaches as t } from "./cache-inspect.js";
|
|
3
|
+
//#region src/sw/session-data.ts
|
|
4
|
+
function n(e) {
|
|
5
|
+
return !e.startsWith("tempest-precache-");
|
|
6
|
+
}
|
|
7
|
+
async function r(r = {}) {
|
|
8
|
+
let [i, a] = await Promise.all([t(r.caches ?? n), e(r.queueName)]);
|
|
9
|
+
return {
|
|
10
|
+
caches: i,
|
|
11
|
+
queueCleared: a
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { r as clearSessionData };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=session-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-data.js","names":[],"sources":["../../src/sw/session-data.ts"],"sourcesContent":["import { clearBackgroundSyncQueue } from \"./background-sync\";\nimport { clearCaches } from \"./cache-inspect\";\nimport type { CacheFilter } from \"./cache-inspect\";\n\n/** Options for {@link clearSessionData}. */\nexport interface ClearSessionDataOptions {\n /**\n * Which Cache Storage buckets hold session data. Default: every bucket except\n * the app-shell precache (`tempest-precache-*`), so the app still launches\n * offline after logout while no runtime entry — whatever its `cacheName` —\n * survives into the next session.\n */\n caches?: CacheFilter;\n /** The background-sync `queueName` to clear. Default `tempest-bg-sync`. */\n queueName?: string;\n}\n\n/** What {@link clearSessionData} removed. */\nexport interface ClearSessionDataResult {\n /** Names of the Cache Storage buckets deleted. */\n caches: string[];\n /** Whether the background-sync queue was emptied. */\n queueCleared: boolean;\n}\n\n/** Default cache filter: everything but the versioned app-shell precache. */\nfunction isSessionCache(name: string): boolean {\n return !name.startsWith(\"tempest-precache-\");\n}\n\n/**\n * Remove everything the service-worker helpers keep on behalf of the signed-in\n * user: the runtime caches filled by `installRuntimeCache` and the queue of\n * mutations waiting in `installBackgroundSync`. Call it on logout, from the page.\n *\n * Both stores outlive the session and are shared by every account that signs in\n * on the browser. Measured in Chromium 153 (11/10/2026), before this existed:\n * the documented `clearCaches(\"tempest-\")` deleted nothing because the recipe's\n * runtime cache was named `api`, the next account read the previous account's\n * `GET /api/me` offline, and a mutation queued by the previous account was\n * replayed with its stored bearer after logout. A prefix filter cannot know the\n * names a recipe chose, so the default here inverts it: every bucket goes except\n * the precache, which holds only the build's static assets.\n *\n * Cache Storage and IndexedDB are origin storage, readable from the window, so\n * this needs no message to the worker.\n *\n * @param options - Narrow the caches to delete, or name a custom queue.\n * @returns The caches deleted and whether the queue was cleared.\n *\n * @example\n * import { clearSessionData } from \"tempest-react-sdk/sw\";\n *\n * async function logout() {\n * await clearSessionData();\n * }\n */\nexport async function clearSessionData(\n options: ClearSessionDataOptions = {},\n): Promise<ClearSessionDataResult> {\n const [caches, queueCleared] = await Promise.all([\n clearCaches(options.caches ?? isSessionCache),\n clearBackgroundSyncQueue(options.queueName),\n ]);\n return { caches, queueCleared };\n}\n"],"mappings":";;;AA0BA,SAAS,EAAe,GAAuB;CAC3C,OAAO,CAAC,EAAK,WAAW,mBAAmB;AAC/C;AA6BA,eAAsB,EAClB,IAAmC,CAAC,GACL;CAC/B,IAAM,CAAC,GAAQ,KAAgB,MAAM,QAAQ,IAAI,CAC7C,EAAY,EAAQ,UAAU,CAAc,GAC5C,EAAyB,EAAQ,SAAS,CAC9C,CAAC;CACD,OAAO;EAAE;EAAQ;CAAa;AAClC"}
|
package/dist/sw.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./sw/register-service-worker.cjs"),t=require("./sw/create-push-handler.cjs"),n=require("./sw/
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./sw/register-service-worker.cjs"),t=require("./sw/create-push-handler.cjs"),n=require("./sw/credential-headers.cjs"),r=require("./sw/cache.cjs"),i=require("./sw/background-sync.cjs"),a=require("./sw/periodic-sync.cjs"),o=require("./sw/cache-inspect.cjs"),s=require("./sw/session-data.cjs");exports.BACKGROUND_SYNC_CREDENTIALS_MESSAGE=i.BACKGROUND_SYNC_CREDENTIALS_MESSAGE,exports.DEFAULT_CREDENTIAL_HEADERS=n.DEFAULT_CREDENTIAL_HEADERS,exports.clearBackgroundSyncQueue=i.clearBackgroundSyncQueue,exports.clearCaches=o.clearCaches,exports.clearSessionData=s.clearSessionData,exports.createPartialResponse=r.createPartialResponse,exports.inspectCaches=o.inspectCaches,exports.installBackgroundSync=i.installBackgroundSync,exports.installNotificationClickHandler=t.installNotificationClickHandler,exports.installPrecache=r.installPrecache,exports.installPushHandler=t.installPushHandler,exports.installRuntimeCache=r.installRuntimeCache,exports.installSkipWaitingListener=t.installSkipWaitingListener,exports.provideBackgroundSyncCredentials=i.provideBackgroundSyncCredentials,exports.registerPeriodicSync=a.registerPeriodicSync,exports.registerServiceWorker=e.registerServiceWorker,exports.skipWaiting=e.skipWaiting,exports.unregisterAllServiceWorkers=e.unregisterAllServiceWorkers;
|
package/dist/sw.d.ts
CHANGED
|
@@ -1,11 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `type` of the message the worker posts to window clients to ask about a queued
|
|
3
|
+
* request. Answered by {@link provideBackgroundSyncCredentials}.
|
|
4
|
+
*/
|
|
5
|
+
export declare const BACKGROUND_SYNC_CREDENTIALS_MESSAGE = "tempest:bg-sync-credentials";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the credentials for a queued request: a header record to apply, or
|
|
9
|
+
* `null` when there is no session (the entry is then dropped).
|
|
10
|
+
*/
|
|
11
|
+
export declare type BackgroundSyncCredentialsProvider = (request: BackgroundSyncCredentialsRequest) => Record<string, string> | null | Promise<Record<string, string> | null>;
|
|
12
|
+
|
|
13
|
+
/** The request a window client is asked about, as sent by the worker. */
|
|
14
|
+
export declare interface BackgroundSyncCredentialsRequest {
|
|
15
|
+
/** Absolute URL of the queued request. */
|
|
16
|
+
url: string;
|
|
17
|
+
/** HTTP method of the queued request. */
|
|
18
|
+
method: string;
|
|
19
|
+
/**
|
|
20
|
+
* `"enqueue"` when the worker only wants the owner of a request that just
|
|
21
|
+
* failed; `"replay"` when it is about to send it and needs the credentials.
|
|
22
|
+
*/
|
|
23
|
+
phase: "enqueue" | "replay";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How an entry is tied to whoever queued it:
|
|
28
|
+
* - `"bound"`: the issuing page reported a principal (or `null` for nobody);
|
|
29
|
+
* the entry only goes out while the same one is signed in.
|
|
30
|
+
* - `"unbound"`: the issuing page answered without `getPrincipal`.
|
|
31
|
+
* - `"unknown"`: nobody could say — no issuing tab, no answer, an error, or an
|
|
32
|
+
* entry written before owners existed. Fails closed.
|
|
33
|
+
*
|
|
34
|
+
* While the worker is still asking, the stored state is `"pending"`, which a
|
|
35
|
+
* replay never sees: it waits, then reads a stamp that never came as `"unknown"`.
|
|
36
|
+
*/
|
|
37
|
+
export declare type BackgroundSyncOwnerState = "bound" | "unbound" | "unknown";
|
|
38
|
+
|
|
39
|
+
/** What a queued request looked like when it was stored, passed to `beforeReplay`. */
|
|
40
|
+
export declare interface BackgroundSyncReplayContext {
|
|
41
|
+
/** Epoch milliseconds of the original failed attempt. */
|
|
42
|
+
queuedAt: number;
|
|
43
|
+
/** Lower-cased credential header names stripped before the entry was stored. */
|
|
44
|
+
strippedHeaders: readonly string[];
|
|
45
|
+
/** How the entry is tied to its owner. */
|
|
46
|
+
ownerState: BackgroundSyncOwnerState;
|
|
47
|
+
/** Owner fingerprint under `"bound"` (`null` for nobody); `undefined` otherwise. */
|
|
48
|
+
owner: string | null | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
1
51
|
/** A cache-name matcher: a prefix string, a `RegExp`, or a predicate. */
|
|
2
52
|
export declare type CacheFilter = string | RegExp | ((name: string) => boolean);
|
|
3
53
|
|
|
4
54
|
/**
|
|
5
55
|
* Main-thread helpers to inspect and clear the Cache Storage buckets a service
|
|
6
|
-
* worker fills — for a "X MB cacheado" readout and a "limpar cache" action
|
|
7
|
-
*
|
|
8
|
-
*
|
|
56
|
+
* worker fills — for a "X MB cacheado" readout and a "limpar cache" action.
|
|
57
|
+
* On logout prefer `clearSessionData`, which also drops the background-sync
|
|
58
|
+
* queue and spares the app-shell precache. All guard `caches` so they no-op
|
|
59
|
+
* safely under SSR / older browsers.
|
|
9
60
|
*/
|
|
10
61
|
/** Per-cache usage summary returned by {@link inspectCaches}. */
|
|
11
62
|
export declare interface CacheReport {
|
|
@@ -17,6 +68,28 @@ export declare interface CacheReport {
|
|
|
17
68
|
bytes: number | null;
|
|
18
69
|
}
|
|
19
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Empty the background-sync queue — every request still waiting to be replayed —
|
|
73
|
+
* and bump its clear epoch. Call it on logout (or use `clearSessionData()`, which
|
|
74
|
+
* also clears the runtime caches) so mutations made under one session are never
|
|
75
|
+
* replayed under the next.
|
|
76
|
+
*
|
|
77
|
+
* Works from the page and from the worker, and reaches work already in flight
|
|
78
|
+
* in the worker: a replay checks each entry right before sending it, and a
|
|
79
|
+
* request still failing when the queue is cleared is not written afterwards.
|
|
80
|
+
*
|
|
81
|
+
* @param queueName - The `queueName` given to {@link installBackgroundSync}.
|
|
82
|
+
* Default `tempest-bg-sync`.
|
|
83
|
+
* @returns `true` once the queue is empty; `false` when IndexedDB is
|
|
84
|
+
* unavailable (SSR, old browsers).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* import { clearBackgroundSyncQueue } from "tempest-react-sdk/sw";
|
|
88
|
+
*
|
|
89
|
+
* await clearBackgroundSyncQueue();
|
|
90
|
+
*/
|
|
91
|
+
export declare function clearBackgroundSyncQueue(queueName?: string): Promise<boolean>;
|
|
92
|
+
|
|
20
93
|
/**
|
|
21
94
|
* Delete Cache Storage buckets whose name passes `filter` (all of them when no
|
|
22
95
|
* filter is given).
|
|
@@ -25,14 +98,64 @@ export declare interface CacheReport {
|
|
|
25
98
|
* @returns The names of the caches that were deleted.
|
|
26
99
|
*
|
|
27
100
|
* @example
|
|
28
|
-
* await clearCaches("tempest-"); // drop
|
|
101
|
+
* await clearCaches("tempest-api"); // drop one runtime bucket
|
|
29
102
|
*/
|
|
30
103
|
export declare function clearCaches(filter?: CacheFilter): Promise<string[]>;
|
|
31
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Remove everything the service-worker helpers keep on behalf of the signed-in
|
|
107
|
+
* user: the runtime caches filled by `installRuntimeCache` and the queue of
|
|
108
|
+
* mutations waiting in `installBackgroundSync`. Call it on logout, from the page.
|
|
109
|
+
*
|
|
110
|
+
* Both stores outlive the session and are shared by every account that signs in
|
|
111
|
+
* on the browser. Measured in Chromium 153 (11/10/2026), before this existed:
|
|
112
|
+
* the documented `clearCaches("tempest-")` deleted nothing because the recipe's
|
|
113
|
+
* runtime cache was named `api`, the next account read the previous account's
|
|
114
|
+
* `GET /api/me` offline, and a mutation queued by the previous account was
|
|
115
|
+
* replayed with its stored bearer after logout. A prefix filter cannot know the
|
|
116
|
+
* names a recipe chose, so the default here inverts it: every bucket goes except
|
|
117
|
+
* the precache, which holds only the build's static assets.
|
|
118
|
+
*
|
|
119
|
+
* Cache Storage and IndexedDB are origin storage, readable from the window, so
|
|
120
|
+
* this needs no message to the worker.
|
|
121
|
+
*
|
|
122
|
+
* @param options - Narrow the caches to delete, or name a custom queue.
|
|
123
|
+
* @returns The caches deleted and whether the queue was cleared.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* import { clearSessionData } from "tempest-react-sdk/sw";
|
|
127
|
+
*
|
|
128
|
+
* async function logout() {
|
|
129
|
+
* await clearSessionData();
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
export declare function clearSessionData(options?: ClearSessionDataOptions): Promise<ClearSessionDataResult>;
|
|
133
|
+
|
|
134
|
+
/** Options for {@link clearSessionData}. */
|
|
135
|
+
export declare interface ClearSessionDataOptions {
|
|
136
|
+
/**
|
|
137
|
+
* Which Cache Storage buckets hold session data. Default: every bucket except
|
|
138
|
+
* the app-shell precache (`tempest-precache-*`), so the app still launches
|
|
139
|
+
* offline after logout while no runtime entry — whatever its `cacheName` —
|
|
140
|
+
* survives into the next session.
|
|
141
|
+
*/
|
|
142
|
+
caches?: CacheFilter;
|
|
143
|
+
/** The background-sync `queueName` to clear. Default `tempest-bg-sync`. */
|
|
144
|
+
queueName?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** What {@link clearSessionData} removed. */
|
|
148
|
+
export declare interface ClearSessionDataResult {
|
|
149
|
+
/** Names of the Cache Storage buckets deleted. */
|
|
150
|
+
caches: string[];
|
|
151
|
+
/** Whether the background-sync queue was emptied. */
|
|
152
|
+
queueCleared: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
32
155
|
/**
|
|
33
156
|
* Build a `206 Partial Content` response from a full one for an HTTP `Range`
|
|
34
157
|
* request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix
|
|
35
|
-
* `bytes=-suffixLength
|
|
158
|
+
* `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable
|
|
36
159
|
* `Range` header, or a `416` when the range is unsatisfiable.
|
|
37
160
|
*
|
|
38
161
|
* @param request The incoming request (its `Range` header drives the slice).
|
|
@@ -40,6 +163,16 @@ export declare function clearCaches(filter?: CacheFilter): Promise<string[]>;
|
|
|
40
163
|
*/
|
|
41
164
|
export declare function createPartialResponse(request: Request, response: Response): Promise<Response>;
|
|
42
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Header names treated as credentials by the service-worker helpers: never
|
|
168
|
+
* persisted by `installBackgroundSync`, and a request carrying any of them
|
|
169
|
+
* bypasses `installRuntimeCache` unless the route opts in. `Cookie` and
|
|
170
|
+
* `Proxy-Authorization` are listed for completeness although the browser
|
|
171
|
+
* forbids scripts from setting them — a cookie session is invisible to the
|
|
172
|
+
* worker, which is why it depends on `Cache-Control` and `clearSessionData()`.
|
|
173
|
+
*/
|
|
174
|
+
export declare const DEFAULT_CREDENTIAL_HEADERS: readonly string[];
|
|
175
|
+
|
|
43
176
|
/**
|
|
44
177
|
* Report entry counts (and optionally byte sizes) for the Cache Storage
|
|
45
178
|
* buckets whose name passes `filter`.
|
|
@@ -60,9 +193,17 @@ export declare function inspectCaches(options?: {
|
|
|
60
193
|
|
|
61
194
|
/**
|
|
62
195
|
* Install the background-sync queue: on a failed mutating request, the request
|
|
63
|
-
* is serialized to IndexedDB and a sync is
|
|
64
|
-
* rejects (so your app can show an offline
|
|
65
|
-
* later when the network returns.
|
|
196
|
+
* is serialized to IndexedDB (without its credential headers) and a sync is
|
|
197
|
+
* registered; the original fetch still rejects (so your app can show an offline
|
|
198
|
+
* state), and the request is replayed later when the network returns.
|
|
199
|
+
*
|
|
200
|
+
* Right after queueing, the worker asks the page that issued the request who is
|
|
201
|
+
* signed in (`getPrincipal` on {@link provideBackgroundSyncCredentials}) and
|
|
202
|
+
* stamps the entry with that owner, without delaying the rejection. The work is
|
|
203
|
+
* handed to `event.waitUntil` synchronously, so the worker stays alive for it
|
|
204
|
+
* in every engine. Without the Background Sync API (every non-Chromium
|
|
205
|
+
* browser), the queue is drained opportunistically whenever a `GET` goes
|
|
206
|
+
* through the worker.
|
|
66
207
|
*
|
|
67
208
|
* @tempest-limits empty-catch — `registration.sync.register` is unavailable on
|
|
68
209
|
* every non-Chromium browser and fails on a revoked background-sync permission.
|
|
@@ -72,20 +213,6 @@ export declare function inspectCaches(options?: {
|
|
|
72
213
|
*/
|
|
73
214
|
export declare function installBackgroundSync(options?: InstallBackgroundSyncOptions): void;
|
|
74
215
|
|
|
75
|
-
/**
|
|
76
|
-
* Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)
|
|
77
|
-
* while offline and replay them when connectivity returns. A dependency-free
|
|
78
|
-
* take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.
|
|
79
|
-
*
|
|
80
|
-
* Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)
|
|
81
|
-
* when available, and also replays opportunistically on the next request as a
|
|
82
|
-
* fallback for browsers without it (e.g. Safari).
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* import { installBackgroundSync } from "tempest-react-sdk/sw";
|
|
86
|
-
*
|
|
87
|
-
* installBackgroundSync({ match: (url) => url.pathname.startsWith("/api/") });
|
|
88
|
-
*/
|
|
89
216
|
/** Options for {@link installBackgroundSync}. */
|
|
90
217
|
export declare interface InstallBackgroundSyncOptions {
|
|
91
218
|
/**
|
|
@@ -105,6 +232,36 @@ export declare interface InstallBackgroundSyncOptions {
|
|
|
105
232
|
* thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.
|
|
106
233
|
*/
|
|
107
234
|
periodicSyncTag?: string;
|
|
235
|
+
/**
|
|
236
|
+
* Header names stripped before a request is persisted — and stripped again on
|
|
237
|
+
* replay, so entries written by an older version never go out with the
|
|
238
|
+
* credential they stored. Default {@link DEFAULT_CREDENTIAL_HEADERS}; extend it
|
|
239
|
+
* for a custom credential header (`[...DEFAULT_CREDENTIAL_HEADERS,
|
|
240
|
+
* "x-tenant-key"]`). Passing `[]` restores the pre-0.77 behavior of
|
|
241
|
+
* persisting and replaying every header, credentials included.
|
|
242
|
+
*/
|
|
243
|
+
credentialHeaders?: readonly string[];
|
|
244
|
+
/**
|
|
245
|
+
* Prepare each queued request right before it is replayed: return the
|
|
246
|
+
* request to send (typically with the current credentials applied) or
|
|
247
|
+
* `null` to drop the entry for good. A thrown error keeps the entry pending.
|
|
248
|
+
* The context carries the entry's owner, so a custom hook can apply the
|
|
249
|
+
* same rule as the default.
|
|
250
|
+
*
|
|
251
|
+
* Default: an entry with no stripped credentials and no bound owner is
|
|
252
|
+
* replayed as-is. The others ask every open window client in parallel (see
|
|
253
|
+
* {@link provideBackgroundSyncCredentials}) and pick the answer that fits:
|
|
254
|
+
* - `"bound"`: the answer whose owner matches; none among valid answers →
|
|
255
|
+
* dropped, never sent anonymously or as somebody else;
|
|
256
|
+
* - `"unknown"`: only an answer without a principal (an app that does not use
|
|
257
|
+
* `getPrincipal`); every answer has one → dropped;
|
|
258
|
+
* - `"unbound"`: any answer, preferring one without a principal.
|
|
259
|
+
* Then `null` headers drop a credentialed entry, an answer that lacks one of
|
|
260
|
+
* the stripped headers keeps it pending, and only the stripped names are
|
|
261
|
+
* re-applied. No valid answer at all keeps it pending until
|
|
262
|
+
* `maxRetentionMinutes` (and warns once per replay in development builds).
|
|
263
|
+
*/
|
|
264
|
+
beforeReplay?: (request: Request, context: BackgroundSyncReplayContext) => Request | null | Promise<Request | null>;
|
|
108
265
|
}
|
|
109
266
|
|
|
110
267
|
/**
|
|
@@ -190,6 +347,14 @@ export declare interface InstallPushHandlerOptions {
|
|
|
190
347
|
* given runtime strategies. Non-matching requests are left untouched (no
|
|
191
348
|
* `respondWith`), so a later {@link installPrecache} can handle them.
|
|
192
349
|
*
|
|
350
|
+
* A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
|
|
351
|
+
* route's `credentialHeaders`) is also left untouched unless its route sets
|
|
352
|
+
* `cacheAuthenticated`, and a response with `Cache-Control: no-store`
|
|
353
|
+
* (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
|
|
354
|
+
* applies to every read, including the offline fallback: an expired entry is a
|
|
355
|
+
* miss, never a last resort. Under `rangeRequests`, the full resource is cached
|
|
356
|
+
* once (the `Range` header stripped) and each range is sliced from it.
|
|
357
|
+
*
|
|
193
358
|
* Register this BEFORE `installPrecache` so specific routes win over the
|
|
194
359
|
* precache catch-all.
|
|
195
360
|
*
|
|
@@ -203,6 +368,57 @@ export declare function installRuntimeCache(routes: RuntimeRoute[]): void;
|
|
|
203
368
|
*/
|
|
204
369
|
export declare function installSkipWaitingListener(): void;
|
|
205
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Answer the worker's questions about queued requests (see `beforeReplay` on
|
|
373
|
+
* {@link installBackgroundSync}). Install it once in every page, next to your
|
|
374
|
+
* auth setup; it reads the token at replay time, so the queue never has to store
|
|
375
|
+
* it. Without it, a queued request that carried a credential header is never
|
|
376
|
+
* replayed.
|
|
377
|
+
*
|
|
378
|
+
* `getHeaders` must return **every** credential header the app sends (the worker
|
|
379
|
+
* keeps an entry pending when one it stripped is missing), or `null` when there
|
|
380
|
+
* is no session — the worker then drops the entry instead of replaying it
|
|
381
|
+
* anonymously. A throw from `getHeaders` or `getPrincipal` is answered as an
|
|
382
|
+
* explicit `{ error: true }`. The worker only applies headers whose names the
|
|
383
|
+
* original request carried, so returning `Authorization` for every URL never
|
|
384
|
+
* leaks it to a URL that did not receive it in the first place.
|
|
385
|
+
*
|
|
386
|
+
* Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
|
|
387
|
+
* the issuing tab right after queueing, asks every tab again before replaying,
|
|
388
|
+
* and drops the entry when no signed-in tab is its owner.
|
|
389
|
+
*
|
|
390
|
+
* @param getHeaders - Resolves the headers for one queued request at replay.
|
|
391
|
+
* @param options - `getPrincipal` binds entries to their owner.
|
|
392
|
+
* @returns A function that removes the listener. A no-op outside the browser.
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* import { provideBackgroundSyncCredentials } from "tempest-react-sdk/sw";
|
|
396
|
+
*
|
|
397
|
+
* const stop = provideBackgroundSyncCredentials(
|
|
398
|
+
* () => {
|
|
399
|
+
* const token = localStorage.getItem("token");
|
|
400
|
+
* return token ? { Authorization: `Bearer ${token}` } : null;
|
|
401
|
+
* },
|
|
402
|
+
* { getPrincipal: () => localStorage.getItem("userId") },
|
|
403
|
+
* );
|
|
404
|
+
*/
|
|
405
|
+
export declare function provideBackgroundSyncCredentials(getHeaders: BackgroundSyncCredentialsProvider, options?: ProvideBackgroundSyncCredentialsOptions): () => void;
|
|
406
|
+
|
|
407
|
+
/** Options for {@link provideBackgroundSyncCredentials}. */
|
|
408
|
+
export declare interface ProvideBackgroundSyncCredentialsOptions {
|
|
409
|
+
/**
|
|
410
|
+
* Who is signed in right now — a stable, non-secret id such as the user id;
|
|
411
|
+
* `null`/`undefined` when nobody is. Read when a request is queued and again
|
|
412
|
+
* when it is replayed: an entry is only sent while the same principal is
|
|
413
|
+
* signed in, and dropped otherwise, so a mutation queued by one account is
|
|
414
|
+
* never sent with another account's credentials even when the app forgets
|
|
415
|
+
* `clearSessionData()` on logout. Only a SHA-256 fingerprint reaches the
|
|
416
|
+
* queue, never the value itself. A throw is answered as an explicit error,
|
|
417
|
+
* which leaves the owner unknown (fail closed).
|
|
418
|
+
*/
|
|
419
|
+
getPrincipal?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
420
|
+
}
|
|
421
|
+
|
|
206
422
|
/**
|
|
207
423
|
* Service-worker context helpers for handling `push` and `notificationclick`
|
|
208
424
|
* events. Import these inside your own `sw.ts` — they expect to run in the
|
|
@@ -390,6 +606,38 @@ export declare interface RuntimeRoute {
|
|
|
390
606
|
* resource is cached once (the `Range` header is stripped before caching).
|
|
391
607
|
*/
|
|
392
608
|
rangeRequests?: boolean;
|
|
609
|
+
/**
|
|
610
|
+
* Opt in to caching requests that carry a credential header (see
|
|
611
|
+
* `credentialHeaders`), and responses marked `Cache-Control: private`.
|
|
612
|
+
* Default `false`.
|
|
613
|
+
*
|
|
614
|
+
* Cache Storage is keyed by URL and outlives the session: without this
|
|
615
|
+
* gate, a `GET /api/me` stored for one account was served to the next
|
|
616
|
+
* account on the same browser whenever the network failed or exceeded
|
|
617
|
+
* `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With
|
|
618
|
+
* the default, credentialed requests bypass the route entirely and reach
|
|
619
|
+
* the network as if no service worker were installed.
|
|
620
|
+
*
|
|
621
|
+
* When enabled, every stored entry gets `Vary` on each credential header the
|
|
622
|
+
* request carried, so the Cache API only matches it for the same credential —
|
|
623
|
+
* another account (or no account) misses instead of reading it. A token
|
|
624
|
+
* rotated while online simply repopulates the entry. Pair it with
|
|
625
|
+
* `clearSessionData()` on logout, which removes the entries themselves.
|
|
626
|
+
* `Cache-Control: no-store` is never cached, opt-in or not.
|
|
627
|
+
*
|
|
628
|
+
* A cookie session is invisible here: the worker cannot see `Cookie`, and
|
|
629
|
+
* gating on `request.credentials` would switch off caching for every
|
|
630
|
+
* same-origin image and font. A cookie-authenticated API stays private only
|
|
631
|
+
* through `Cache-Control: private`/`no-store` on its responses and
|
|
632
|
+
* `clearSessionData()` on logout.
|
|
633
|
+
*/
|
|
634
|
+
cacheAuthenticated?: boolean;
|
|
635
|
+
/**
|
|
636
|
+
* Header names that mark a request as credentialed. Default
|
|
637
|
+
* `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,
|
|
638
|
+
* `x-auth-token`, …) — the same list the background-sync queue strips.
|
|
639
|
+
*/
|
|
640
|
+
credentialHeaders?: readonly string[];
|
|
393
641
|
}
|
|
394
642
|
|
|
395
643
|
/**
|
|
@@ -414,7 +662,7 @@ export declare interface RuntimeRoute {
|
|
|
414
662
|
*
|
|
415
663
|
* // Register specific routes FIRST so they win over the precache catch-all.
|
|
416
664
|
* installRuntimeCache([
|
|
417
|
-
* { match: /\/api\//, strategy: "network-first", cacheName: "api", maxAgeSeconds: 300 },
|
|
665
|
+
* { match: /\/api\//, strategy: "network-first", cacheName: "tempest-api", maxAgeSeconds: 300 },
|
|
418
666
|
* ]);
|
|
419
667
|
* installPrecache();
|
|
420
668
|
*/
|
package/dist/sw.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { registerServiceWorker as e, skipWaiting as t, unregisterAllServiceWorkers as n } from "./sw/register-service-worker.js";
|
|
2
2
|
import { installNotificationClickHandler as r, installPushHandler as i, installSkipWaitingListener as a } from "./sw/create-push-handler.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { DEFAULT_CREDENTIAL_HEADERS as o } from "./sw/credential-headers.js";
|
|
4
|
+
import { createPartialResponse as s, installPrecache as c, installRuntimeCache as l } from "./sw/cache.js";
|
|
5
|
+
import { BACKGROUND_SYNC_CREDENTIALS_MESSAGE as u, clearBackgroundSyncQueue as d, installBackgroundSync as f, provideBackgroundSyncCredentials as p } from "./sw/background-sync.js";
|
|
6
|
+
import { registerPeriodicSync as m } from "./sw/periodic-sync.js";
|
|
7
|
+
import { clearCaches as h, inspectCaches as g } from "./sw/cache-inspect.js";
|
|
8
|
+
import { clearSessionData as _ } from "./sw/session-data.js";
|
|
9
|
+
export { u as BACKGROUND_SYNC_CREDENTIALS_MESSAGE, o as DEFAULT_CREDENTIAL_HEADERS, d as clearBackgroundSyncQueue, h as clearCaches, _ as clearSessionData, s as createPartialResponse, g as inspectCaches, f as installBackgroundSync, r as installNotificationClickHandler, c as installPrecache, i as installPushHandler, l as installRuntimeCache, a as installSkipWaitingListener, p as provideBackgroundSyncCredentials, m as registerPeriodicSync, e as registerServiceWorker, t as skipWaiting, n as unregisterAllServiceWorkers };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=2166136261,t=16777619;function n(e,n){let r=n;for(let n=0;n<e.length;n+=1)r^=e.charCodeAt(n),r=Math.imul(r,t);return r>>>0}function r(t){return`${n(t,e).toString(16).padStart(8,`0`)}${n(t,2128831034).toString(16).padStart(8,`0`)}`}function i(e,t){let n=typeof t==`string`?t:typeof t==`number`&&Number.isFinite(t)?String(t):``;return n?`${e}:${r(n)}`:null}exports.FNV_OFFSET_BASIS_32=e,exports.FNV_PRIME_32=t,exports.appErrorOwner=i,exports.fingerprintAppErrorOwner=r;
|
|
2
|
+
//# sourceMappingURL=app-error-owner.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-error-owner.cjs","names":[],"sources":["../../src/telemetry/app-error-owner.ts"],"sourcesContent":["/**\n * FNV-1a 32-bit offset basis, the seed of the first half of the fingerprint.\n *\n * Ported from the FNV specification (Fowler/Noll/Vo, `offset_basis` for 32 bits).\n */\nexport const FNV_OFFSET_BASIS_32 = 0x811c9dc5;\n\n/**\n * FNV-1a 32-bit prime.\n *\n * Ported from the FNV specification (Fowler/Noll/Vo, `FNV_prime` for 32 bits).\n */\nexport const FNV_PRIME_32 = 0x01000193;\n\n/**\n * Seed of the second half of the fingerprint: the basis with every bit\n * flipped, so the two halves are independent hashes of the same text.\n */\nconst SECOND_SEED = ~FNV_OFFSET_BASIS_32 >>> 0;\n\n/**\n * Hash `text` with FNV-1a from `seed`.\n *\n * @param text - What to hash, read as UTF-16 code units.\n * @param seed - Starting value.\n * @returns The 32-bit hash as an unsigned integer.\n */\nfunction fnv1a(text: string, seed: number): number {\n let hash = seed;\n for (let index = 0; index < text.length; index += 1) {\n hash ^= text.charCodeAt(index);\n hash = Math.imul(hash, FNV_PRIME_32);\n }\n return hash >>> 0;\n}\n\n/**\n * A 64-bit one-way fingerprint of `text`, as 16 hex characters.\n *\n * Not a cryptographic hash, and it does not need to be: it only has to tell\n * two signed-in identities apart on one device, and it must not let anyone\n * holding the stored queue send as the user. Sixty-four bits cannot be turned\n * back into a bearer token, and two different users colliding on one browser\n * is a one-in-2^64 event. A synchronous hash is what lets `report()` stamp the\n * owner without awaiting `crypto.subtle`.\n *\n * @param text - The principal or token to fingerprint.\n * @returns Sixteen lowercase hex characters.\n */\nexport function fingerprintAppErrorOwner(text: string): string {\n const first = fnv1a(text, FNV_OFFSET_BASIS_32).toString(16).padStart(8, \"0\");\n const second = fnv1a(text, SECOND_SEED).toString(16).padStart(8, \"0\");\n return `${first}${second}`;\n}\n\n/**\n * Turn a principal or a token into the fingerprint a queue entry stores.\n *\n * The prefix keeps the two sources from ever matching each other: a report\n * stamped from a principal is never taken for one stamped from a token.\n *\n * Only a non-empty string or a finite number is an identity. Anything else —\n * notably the `Promise` an `async` getter returns from untyped JS — reads as\n * nobody, because stringifying it would give every user the same\n * `\"[object Promise]\"` fingerprint.\n *\n * @param source - `\"p\"` for a principal from `getPrincipal`, `\"t\"` for a token.\n * @param value - The identity, or nothing when nobody is signed in.\n * @returns `\"<source>:<fingerprint>\"`, or `null` when `value` is no identity.\n */\nexport function appErrorOwner(source: \"p\" | \"t\", value: unknown): string | null {\n const text =\n typeof value === \"string\"\n ? value\n : typeof value === \"number\" && Number.isFinite(value)\n ? String(value)\n : \"\";\n return text ? `${source}:${fingerprintAppErrorOwner(text)}` : null;\n}\n"],"mappings":"AAKA,IAAa,EAAsB,WAOtB,EAAe,SAe5B,SAAS,EAAM,EAAc,EAAsB,CAC/C,IAAI,EAAO,EACX,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,GAAS,EAC9C,GAAQ,EAAK,WAAW,CAAK,EAC7B,EAAO,KAAK,KAAK,EAAM,CAAY,EAEvC,OAAO,IAAS,CACpB,CAeA,SAAgB,EAAyB,EAAsB,CAG3D,MAAO,GAFO,EAAM,EAAM,CAAmB,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAE9D,IADK,EAAM,EAAM,UAAW,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAC/C,GACtB,CAiBA,SAAgB,EAAc,EAAmB,EAA+B,CAC5E,IAAM,EACF,OAAO,GAAU,SACX,EACA,OAAO,GAAU,UAAY,OAAO,SAAS,CAAK,EAChD,OAAO,CAAK,EACZ,GACZ,OAAO,EAAO,GAAG,EAAO,GAAG,EAAyB,CAAI,IAAM,IAClE"}
|