tempest-react-sdk 0.76.0 → 0.78.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 +2 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/alias/collect.mjs +20 -3
- package/bin/lib/alias/index.mjs +12 -1
- package/bin/lib/alias/index.test.mjs +24 -0
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/css/analyze.test.mjs +21 -1
- package/bin/lib/css/collect.mjs +14 -0
- package/bin/lib/css/extract.mjs +63 -5
- package/bin/lib/css/extract.test.mjs +110 -1
- package/bin/lib/css/fix.e2e.test.mjs +50 -0
- package/bin/lib/css/index.mjs +6 -1
- package/bin/lib/fs/contain.mjs +50 -0
- package/bin/lib/icons/generate.mjs +28 -3
- package/bin/lib/icons/generate.test.mjs +57 -1
- package/bin/lib/openapi/escape.mjs +224 -0
- package/bin/lib/openapi/generate.encode.test.mjs +120 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +621 -0
- package/bin/lib/openapi/generate.mjs +129 -18
- package/bin/lib/openapi/generate.test.mjs +7 -2
- package/bin/lib/openapi/schema-to-zod.mjs +43 -4
- package/bin/tempest-gen.test.mjs +188 -0
- package/bin/tempest.mjs +263 -6
- 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 +203 -58
- 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 +85 -72
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +19 -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/CodeBlock/CodeBlock.cjs +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/CodeBlock/tokenize.cjs +1 -1
- package/dist/components/CodeBlock/tokenize.cjs.map +1 -1
- package/dist/components/CodeBlock/tokenize.js +54 -25
- package/dist/components/CodeBlock/tokenize.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 +171 -90
- 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/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +141 -105
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +5 -6
- package/dist/http/retry.js.map +1 -1
- 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 +272 -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 +309 -23
- package/dist/sw.js +7 -5
- 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 +1183 -64
- 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 +18 -14
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/owner-fingerprint.cjs +2 -0
- package/dist/utils/owner-fingerprint.cjs.map +1 -0
- package/dist/utils/owner-fingerprint.js +18 -0
- package/dist/utils/owner-fingerprint.js.map +1 -0
- 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 +130 -64
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +23 -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 +1 -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. The runtime cache therefore depends on `Cache-Control` and\n * `clearSessionData()`; the background-sync queue checks the entry's owner\n * through `getPrincipal` whether or not a credential header was stripped, and\n * `clearSessionData()` still backs that check up.\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":"AAWA,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. The runtime cache therefore depends on `Cache-Control` and\n * `clearSessionData()`; the background-sync queue checks the entry's owner\n * through `getPrincipal` whether or not a credential header was stripped, and\n * `clearSessionData()` still backs that check up.\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":";AAWA,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,76 @@
|
|
|
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 the default `beforeReplay` treats an `"unknown"` owner on an entry that
|
|
28
|
+
* carried no credential header:
|
|
29
|
+
* - `"strict"` (default): it asks the open pages like any other entry — replayed
|
|
30
|
+
* only when every answer lacks a principal, dropped as soon as one answer has
|
|
31
|
+
* one, pending while nobody answers.
|
|
32
|
+
* - `"lenient"`: it is replayed as-is, without asking (the behavior up to 0.77.0).
|
|
33
|
+
*
|
|
34
|
+
* An entry with stripped credentials always asks, under either policy.
|
|
35
|
+
*/
|
|
36
|
+
export declare type BackgroundSyncOwnerPolicy = "strict" | "lenient";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How an entry is tied to whoever queued it:
|
|
40
|
+
* - `"bound"`: the issuing page reported a principal (or `null` for nobody);
|
|
41
|
+
* the entry only goes out while the same one is signed in.
|
|
42
|
+
* - `"unbound"`: the issuing page answered without `getPrincipal`.
|
|
43
|
+
* - `"unknown"`: nobody could say — no issuing tab, no answer, an error, or an
|
|
44
|
+
* entry written before owners existed. Fails closed, with or without stripped
|
|
45
|
+
* credentials: a cookie session rides along on the replay, so an entry with no
|
|
46
|
+
* credential header is checked like any other.
|
|
47
|
+
*
|
|
48
|
+
* While the worker is still asking, the stored state is `"pending"`, which a
|
|
49
|
+
* replay never sees: it waits, then reads a stamp that never came as `"unknown"`.
|
|
50
|
+
*/
|
|
51
|
+
export declare type BackgroundSyncOwnerState = "bound" | "unbound" | "unknown";
|
|
52
|
+
|
|
53
|
+
/** What a queued request looked like when it was stored, passed to `beforeReplay`. */
|
|
54
|
+
export declare interface BackgroundSyncReplayContext {
|
|
55
|
+
/** Epoch milliseconds of the original failed attempt. */
|
|
56
|
+
queuedAt: number;
|
|
57
|
+
/** Lower-cased credential header names stripped before the entry was stored. */
|
|
58
|
+
strippedHeaders: readonly string[];
|
|
59
|
+
/** How the entry is tied to its owner. */
|
|
60
|
+
ownerState: BackgroundSyncOwnerState;
|
|
61
|
+
/** Owner fingerprint under `"bound"` (`null` for nobody); `undefined` otherwise. */
|
|
62
|
+
owner: string | null | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
1
65
|
/** A cache-name matcher: a prefix string, a `RegExp`, or a predicate. */
|
|
2
66
|
export declare type CacheFilter = string | RegExp | ((name: string) => boolean);
|
|
3
67
|
|
|
4
68
|
/**
|
|
5
69
|
* 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
|
-
*
|
|
70
|
+
* worker fills — for a "X MB cacheado" readout and a "limpar cache" action.
|
|
71
|
+
* On logout prefer `clearSessionData`, which also drops the background-sync
|
|
72
|
+
* queue and spares the app-shell precache. All guard `caches` so they no-op
|
|
73
|
+
* safely under SSR / older browsers.
|
|
9
74
|
*/
|
|
10
75
|
/** Per-cache usage summary returned by {@link inspectCaches}. */
|
|
11
76
|
export declare interface CacheReport {
|
|
@@ -17,6 +82,28 @@ export declare interface CacheReport {
|
|
|
17
82
|
bytes: number | null;
|
|
18
83
|
}
|
|
19
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Empty the background-sync queue — every request still waiting to be replayed —
|
|
87
|
+
* and bump its clear epoch. Call it on logout (or use `clearSessionData()`, which
|
|
88
|
+
* also clears the runtime caches) so mutations made under one session are never
|
|
89
|
+
* replayed under the next.
|
|
90
|
+
*
|
|
91
|
+
* Works from the page and from the worker, and reaches work already in flight
|
|
92
|
+
* in the worker: a replay checks each entry right before sending it, and a
|
|
93
|
+
* request still failing when the queue is cleared is not written afterwards.
|
|
94
|
+
*
|
|
95
|
+
* @param queueName - The `queueName` given to {@link installBackgroundSync}.
|
|
96
|
+
* Default `tempest-bg-sync`.
|
|
97
|
+
* @returns `true` once the queue is empty; `false` when IndexedDB is
|
|
98
|
+
* unavailable (SSR, old browsers).
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* import { clearBackgroundSyncQueue } from "tempest-react-sdk/sw";
|
|
102
|
+
*
|
|
103
|
+
* await clearBackgroundSyncQueue();
|
|
104
|
+
*/
|
|
105
|
+
export declare function clearBackgroundSyncQueue(queueName?: string): Promise<boolean>;
|
|
106
|
+
|
|
20
107
|
/**
|
|
21
108
|
* Delete Cache Storage buckets whose name passes `filter` (all of them when no
|
|
22
109
|
* filter is given).
|
|
@@ -25,14 +112,64 @@ export declare interface CacheReport {
|
|
|
25
112
|
* @returns The names of the caches that were deleted.
|
|
26
113
|
*
|
|
27
114
|
* @example
|
|
28
|
-
* await clearCaches("tempest-"); // drop
|
|
115
|
+
* await clearCaches("tempest-api"); // drop one runtime bucket
|
|
29
116
|
*/
|
|
30
117
|
export declare function clearCaches(filter?: CacheFilter): Promise<string[]>;
|
|
31
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Remove everything the service-worker helpers keep on behalf of the signed-in
|
|
121
|
+
* user: the runtime caches filled by `installRuntimeCache` and the queue of
|
|
122
|
+
* mutations waiting in `installBackgroundSync`. Call it on logout, from the page.
|
|
123
|
+
*
|
|
124
|
+
* Both stores outlive the session and are shared by every account that signs in
|
|
125
|
+
* on the browser. Measured in Chromium 153 (11/10/2026), before this existed:
|
|
126
|
+
* the documented `clearCaches("tempest-")` deleted nothing because the recipe's
|
|
127
|
+
* runtime cache was named `api`, the next account read the previous account's
|
|
128
|
+
* `GET /api/me` offline, and a mutation queued by the previous account was
|
|
129
|
+
* replayed with its stored bearer after logout. A prefix filter cannot know the
|
|
130
|
+
* names a recipe chose, so the default here inverts it: every bucket goes except
|
|
131
|
+
* the precache, which holds only the build's static assets.
|
|
132
|
+
*
|
|
133
|
+
* Cache Storage and IndexedDB are origin storage, readable from the window, so
|
|
134
|
+
* this needs no message to the worker.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Narrow the caches to delete, or name a custom queue.
|
|
137
|
+
* @returns The caches deleted and whether the queue was cleared.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* import { clearSessionData } from "tempest-react-sdk/sw";
|
|
141
|
+
*
|
|
142
|
+
* async function logout() {
|
|
143
|
+
* await clearSessionData();
|
|
144
|
+
* }
|
|
145
|
+
*/
|
|
146
|
+
export declare function clearSessionData(options?: ClearSessionDataOptions): Promise<ClearSessionDataResult>;
|
|
147
|
+
|
|
148
|
+
/** Options for {@link clearSessionData}. */
|
|
149
|
+
export declare interface ClearSessionDataOptions {
|
|
150
|
+
/**
|
|
151
|
+
* Which Cache Storage buckets hold session data. Default: every bucket except
|
|
152
|
+
* the app-shell precache (`tempest-precache-*`), so the app still launches
|
|
153
|
+
* offline after logout while no runtime entry — whatever its `cacheName` —
|
|
154
|
+
* survives into the next session.
|
|
155
|
+
*/
|
|
156
|
+
caches?: CacheFilter;
|
|
157
|
+
/** The background-sync `queueName` to clear. Default `tempest-bg-sync`. */
|
|
158
|
+
queueName?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** What {@link clearSessionData} removed. */
|
|
162
|
+
export declare interface ClearSessionDataResult {
|
|
163
|
+
/** Names of the Cache Storage buckets deleted. */
|
|
164
|
+
caches: string[];
|
|
165
|
+
/** Whether the background-sync queue was emptied. */
|
|
166
|
+
queueCleared: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
32
169
|
/**
|
|
33
170
|
* Build a `206 Partial Content` response from a full one for an HTTP `Range`
|
|
34
171
|
* request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix
|
|
35
|
-
* `bytes=-suffixLength
|
|
172
|
+
* `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable
|
|
36
173
|
* `Range` header, or a `416` when the range is unsatisfiable.
|
|
37
174
|
*
|
|
38
175
|
* @param request The incoming request (its `Range` header drives the slice).
|
|
@@ -40,6 +177,19 @@ export declare function clearCaches(filter?: CacheFilter): Promise<string[]>;
|
|
|
40
177
|
*/
|
|
41
178
|
export declare function createPartialResponse(request: Request, response: Response): Promise<Response>;
|
|
42
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Header names treated as credentials by the service-worker helpers: never
|
|
182
|
+
* persisted by `installBackgroundSync`, and a request carrying any of them
|
|
183
|
+
* bypasses `installRuntimeCache` unless the route opts in. `Cookie` and
|
|
184
|
+
* `Proxy-Authorization` are listed for completeness although the browser
|
|
185
|
+
* forbids scripts from setting them — a cookie session is invisible to the
|
|
186
|
+
* worker. The runtime cache therefore depends on `Cache-Control` and
|
|
187
|
+
* `clearSessionData()`; the background-sync queue checks the entry's owner
|
|
188
|
+
* through `getPrincipal` whether or not a credential header was stripped, and
|
|
189
|
+
* `clearSessionData()` still backs that check up.
|
|
190
|
+
*/
|
|
191
|
+
export declare const DEFAULT_CREDENTIAL_HEADERS: readonly string[];
|
|
192
|
+
|
|
43
193
|
/**
|
|
44
194
|
* Report entry counts (and optionally byte sizes) for the Cache Storage
|
|
45
195
|
* buckets whose name passes `filter`.
|
|
@@ -60,9 +210,17 @@ export declare function inspectCaches(options?: {
|
|
|
60
210
|
|
|
61
211
|
/**
|
|
62
212
|
* 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.
|
|
213
|
+
* is serialized to IndexedDB (without its credential headers) and a sync is
|
|
214
|
+
* registered; the original fetch still rejects (so your app can show an offline
|
|
215
|
+
* state), and the request is replayed later when the network returns.
|
|
216
|
+
*
|
|
217
|
+
* Right after queueing, the worker asks the page that issued the request who is
|
|
218
|
+
* signed in (`getPrincipal` on {@link provideBackgroundSyncCredentials}) and
|
|
219
|
+
* stamps the entry with that owner, without delaying the rejection. The work is
|
|
220
|
+
* handed to `event.waitUntil` synchronously, so the worker stays alive for it
|
|
221
|
+
* in every engine. Without the Background Sync API (every non-Chromium
|
|
222
|
+
* browser), the queue is drained opportunistically whenever a `GET` goes
|
|
223
|
+
* through the worker.
|
|
66
224
|
*
|
|
67
225
|
* @tempest-limits empty-catch — `registration.sync.register` is unavailable on
|
|
68
226
|
* every non-Chromium browser and fails on a revoked background-sync permission.
|
|
@@ -72,20 +230,6 @@ export declare function inspectCaches(options?: {
|
|
|
72
230
|
*/
|
|
73
231
|
export declare function installBackgroundSync(options?: InstallBackgroundSyncOptions): void;
|
|
74
232
|
|
|
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
233
|
/** Options for {@link installBackgroundSync}. */
|
|
90
234
|
export declare interface InstallBackgroundSyncOptions {
|
|
91
235
|
/**
|
|
@@ -105,6 +249,57 @@ export declare interface InstallBackgroundSyncOptions {
|
|
|
105
249
|
* thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.
|
|
106
250
|
*/
|
|
107
251
|
periodicSyncTag?: string;
|
|
252
|
+
/**
|
|
253
|
+
* Header names stripped before a request is persisted — and stripped again on
|
|
254
|
+
* replay, so entries written by an older version never go out with the
|
|
255
|
+
* credential they stored. Default {@link DEFAULT_CREDENTIAL_HEADERS}; extend it
|
|
256
|
+
* for a custom credential header (`[...DEFAULT_CREDENTIAL_HEADERS,
|
|
257
|
+
* "x-tenant-key"]`). Passing `[]` restores the pre-0.77 behavior of
|
|
258
|
+
* persisting and replaying every header, credentials included.
|
|
259
|
+
*/
|
|
260
|
+
credentialHeaders?: readonly string[];
|
|
261
|
+
/**
|
|
262
|
+
* Prepare each queued request right before it is replayed: return the
|
|
263
|
+
* request to send (typically with the current credentials applied) or
|
|
264
|
+
* `null` to drop the entry for good. A thrown error keeps the entry pending.
|
|
265
|
+
* The context carries the entry's owner, so a custom hook can apply the
|
|
266
|
+
* same rule as the default.
|
|
267
|
+
*
|
|
268
|
+
* Default: only an `"unbound"` entry with no stripped credentials (and, under
|
|
269
|
+
* `ownerPolicy: "lenient"`, an `"unknown"` one) is replayed as-is, without
|
|
270
|
+
* asking. Every other entry — credentialed or not —
|
|
271
|
+
* asks every open window client in parallel (see
|
|
272
|
+
* {@link provideBackgroundSyncCredentials}) and picks the answer that fits:
|
|
273
|
+
* - `"bound"`: the answer whose owner matches; none among valid answers →
|
|
274
|
+
* dropped, never sent anonymously or as somebody else;
|
|
275
|
+
* - `"unknown"`: only when every answer lacks a principal (an app that does
|
|
276
|
+
* not use `getPrincipal`); a single answer with one → dropped, since a
|
|
277
|
+
* stale tab without `getPrincipal` says nothing about who is signed in
|
|
278
|
+
* next to it. This holds for an entry
|
|
279
|
+
* with no credential header too, since the browser attaches the cookies of
|
|
280
|
+
* whoever is signed in at replay time;
|
|
281
|
+
* - `"unbound"`: any answer, preferring one without a principal.
|
|
282
|
+
* Then `null` headers drop a credentialed entry, an answer that lacks one of
|
|
283
|
+
* the stripped headers keeps it pending, and only the stripped names are
|
|
284
|
+
* re-applied. No valid answer at all keeps it pending until
|
|
285
|
+
* `maxRetentionMinutes` (and warns once per replay in development builds) —
|
|
286
|
+
* so a page that never calls {@link provideBackgroundSyncCredentials} leaves
|
|
287
|
+
* every entry it queued `"unknown"`, and none of them is replayed.
|
|
288
|
+
*/
|
|
289
|
+
/**
|
|
290
|
+
* How an `"unknown"` owner is handled on an entry with no stripped
|
|
291
|
+
* credentials (see {@link BackgroundSyncOwnerPolicy}). Default `"strict"`.
|
|
292
|
+
*
|
|
293
|
+
* `"lenient"` restores the replay-as-is of 0.77.0 and earlier for an app without sign-in,
|
|
294
|
+
* where no page ever reports a principal. Do not use it with a cookie
|
|
295
|
+
* session: the browser attaches on replay the cookie of whoever is signed in
|
|
296
|
+
* at that moment, so a mutation queued by A whose owner was never stamped
|
|
297
|
+
* (issuing tab closed, empty `clientId`, stamp lost, legacy entry) goes out
|
|
298
|
+
* with B's session. Entries with stripped credentials ask the pages either
|
|
299
|
+
* way. Ignored when a custom `beforeReplay` is given.
|
|
300
|
+
*/
|
|
301
|
+
ownerPolicy?: BackgroundSyncOwnerPolicy;
|
|
302
|
+
beforeReplay?: (request: Request, context: BackgroundSyncReplayContext) => Request | null | Promise<Request | null>;
|
|
108
303
|
}
|
|
109
304
|
|
|
110
305
|
/**
|
|
@@ -190,6 +385,14 @@ export declare interface InstallPushHandlerOptions {
|
|
|
190
385
|
* given runtime strategies. Non-matching requests are left untouched (no
|
|
191
386
|
* `respondWith`), so a later {@link installPrecache} can handle them.
|
|
192
387
|
*
|
|
388
|
+
* A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
|
|
389
|
+
* route's `credentialHeaders`) is also left untouched unless its route sets
|
|
390
|
+
* `cacheAuthenticated`, and a response with `Cache-Control: no-store`
|
|
391
|
+
* (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
|
|
392
|
+
* applies to every read, including the offline fallback: an expired entry is a
|
|
393
|
+
* miss, never a last resort. Under `rangeRequests`, the full resource is cached
|
|
394
|
+
* once (the `Range` header stripped) and each range is sliced from it.
|
|
395
|
+
*
|
|
193
396
|
* Register this BEFORE `installPrecache` so specific routes win over the
|
|
194
397
|
* precache catch-all.
|
|
195
398
|
*
|
|
@@ -203,6 +406,57 @@ export declare function installRuntimeCache(routes: RuntimeRoute[]): void;
|
|
|
203
406
|
*/
|
|
204
407
|
export declare function installSkipWaitingListener(): void;
|
|
205
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Answer the worker's questions about queued requests (see `beforeReplay` on
|
|
411
|
+
* {@link installBackgroundSync}). Install it once in every page, next to your
|
|
412
|
+
* auth setup; it reads the token at replay time, so the queue never has to store
|
|
413
|
+
* it. Without it, a queued request that carried a credential header is never
|
|
414
|
+
* replayed.
|
|
415
|
+
*
|
|
416
|
+
* `getHeaders` must return **every** credential header the app sends (the worker
|
|
417
|
+
* keeps an entry pending when one it stripped is missing), or `null` when there
|
|
418
|
+
* is no session — the worker then drops the entry instead of replaying it
|
|
419
|
+
* anonymously. A throw from `getHeaders` or `getPrincipal` is answered as an
|
|
420
|
+
* explicit `{ error: true }`. The worker only applies headers whose names the
|
|
421
|
+
* original request carried, so returning `Authorization` for every URL never
|
|
422
|
+
* leaks it to a URL that did not receive it in the first place.
|
|
423
|
+
*
|
|
424
|
+
* Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
|
|
425
|
+
* the issuing tab right after queueing, asks every tab again before replaying,
|
|
426
|
+
* and drops the entry when no signed-in tab is its owner.
|
|
427
|
+
*
|
|
428
|
+
* @param getHeaders - Resolves the headers for one queued request at replay.
|
|
429
|
+
* @param options - `getPrincipal` binds entries to their owner.
|
|
430
|
+
* @returns A function that removes the listener. A no-op outside the browser.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* import { provideBackgroundSyncCredentials } from "tempest-react-sdk/sw";
|
|
434
|
+
*
|
|
435
|
+
* const stop = provideBackgroundSyncCredentials(
|
|
436
|
+
* () => {
|
|
437
|
+
* const token = localStorage.getItem("token");
|
|
438
|
+
* return token ? { Authorization: `Bearer ${token}` } : null;
|
|
439
|
+
* },
|
|
440
|
+
* { getPrincipal: () => localStorage.getItem("userId") },
|
|
441
|
+
* );
|
|
442
|
+
*/
|
|
443
|
+
export declare function provideBackgroundSyncCredentials(getHeaders: BackgroundSyncCredentialsProvider, options?: ProvideBackgroundSyncCredentialsOptions): () => void;
|
|
444
|
+
|
|
445
|
+
/** Options for {@link provideBackgroundSyncCredentials}. */
|
|
446
|
+
export declare interface ProvideBackgroundSyncCredentialsOptions {
|
|
447
|
+
/**
|
|
448
|
+
* Who is signed in right now — a stable, non-secret id such as the user id;
|
|
449
|
+
* `null`/`undefined` when nobody is. Read when a request is queued and again
|
|
450
|
+
* when it is replayed: an entry is only sent while the same principal is
|
|
451
|
+
* signed in, and dropped otherwise, so a mutation queued by one account is
|
|
452
|
+
* never sent with another account's credentials even when the app forgets
|
|
453
|
+
* `clearSessionData()` on logout. Only a SHA-256 fingerprint reaches the
|
|
454
|
+
* queue, never the value itself. A throw is answered as an explicit error,
|
|
455
|
+
* which leaves the owner unknown (fail closed).
|
|
456
|
+
*/
|
|
457
|
+
getPrincipal?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
458
|
+
}
|
|
459
|
+
|
|
206
460
|
/**
|
|
207
461
|
* Service-worker context helpers for handling `push` and `notificationclick`
|
|
208
462
|
* events. Import these inside your own `sw.ts` — they expect to run in the
|
|
@@ -390,6 +644,38 @@ export declare interface RuntimeRoute {
|
|
|
390
644
|
* resource is cached once (the `Range` header is stripped before caching).
|
|
391
645
|
*/
|
|
392
646
|
rangeRequests?: boolean;
|
|
647
|
+
/**
|
|
648
|
+
* Opt in to caching requests that carry a credential header (see
|
|
649
|
+
* `credentialHeaders`), and responses marked `Cache-Control: private`.
|
|
650
|
+
* Default `false`.
|
|
651
|
+
*
|
|
652
|
+
* Cache Storage is keyed by URL and outlives the session: without this
|
|
653
|
+
* gate, a `GET /api/me` stored for one account was served to the next
|
|
654
|
+
* account on the same browser whenever the network failed or exceeded
|
|
655
|
+
* `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With
|
|
656
|
+
* the default, credentialed requests bypass the route entirely and reach
|
|
657
|
+
* the network as if no service worker were installed.
|
|
658
|
+
*
|
|
659
|
+
* When enabled, every stored entry gets `Vary` on each credential header the
|
|
660
|
+
* request carried, so the Cache API only matches it for the same credential —
|
|
661
|
+
* another account (or no account) misses instead of reading it. A token
|
|
662
|
+
* rotated while online simply repopulates the entry. Pair it with
|
|
663
|
+
* `clearSessionData()` on logout, which removes the entries themselves.
|
|
664
|
+
* `Cache-Control: no-store` is never cached, opt-in or not.
|
|
665
|
+
*
|
|
666
|
+
* A cookie session is invisible here: the worker cannot see `Cookie`, and
|
|
667
|
+
* gating on `request.credentials` would switch off caching for every
|
|
668
|
+
* same-origin image and font. A cookie-authenticated API stays private only
|
|
669
|
+
* through `Cache-Control: private`/`no-store` on its responses and
|
|
670
|
+
* `clearSessionData()` on logout.
|
|
671
|
+
*/
|
|
672
|
+
cacheAuthenticated?: boolean;
|
|
673
|
+
/**
|
|
674
|
+
* Header names that mark a request as credentialed. Default
|
|
675
|
+
* `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,
|
|
676
|
+
* `x-auth-token`, …) — the same list the background-sync queue strips.
|
|
677
|
+
*/
|
|
678
|
+
credentialHeaders?: readonly string[];
|
|
393
679
|
}
|
|
394
680
|
|
|
395
681
|
/**
|
|
@@ -414,7 +700,7 @@ export declare interface RuntimeRoute {
|
|
|
414
700
|
*
|
|
415
701
|
* // Register specific routes FIRST so they win over the precache catch-all.
|
|
416
702
|
* installRuntimeCache([
|
|
417
|
-
* { match: /\/api\//, strategy: "network-first", cacheName: "api", maxAgeSeconds: 300 },
|
|
703
|
+
* { match: /\/api\//, strategy: "network-first", cacheName: "tempest-api", maxAgeSeconds: 300 },
|
|
418
704
|
* ]);
|
|
419
705
|
* installPrecache();
|
|
420
706
|
*/
|