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.
Files changed (147) hide show
  1. package/README.md +4 -2
  2. package/bin/create-tempest-app.e2e.test.mjs +112 -0
  3. package/bin/create-tempest-app.mjs +69 -28
  4. package/bin/lib/component/generate.mjs +191 -0
  5. package/bin/lib/component/generate.test.mjs +336 -0
  6. package/bin/lib/openapi/escape.mjs +210 -0
  7. package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
  8. package/bin/lib/openapi/generate.mjs +93 -17
  9. package/bin/lib/openapi/schema-to-zod.mjs +41 -4
  10. package/bin/tempest.mjs +184 -3
  11. package/dist/auth/create-tempest-auth.cjs +1 -1
  12. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  13. package/dist/auth/create-tempest-auth.js +127 -55
  14. package/dist/auth/create-tempest-auth.js.map +1 -1
  15. package/dist/auth/refresh-queue.cjs +1 -1
  16. package/dist/auth/refresh-queue.cjs.map +1 -1
  17. package/dist/auth/refresh-queue.js +13 -6
  18. package/dist/auth/refresh-queue.js.map +1 -1
  19. package/dist/br/pix.cjs +1 -1
  20. package/dist/br/pix.cjs.map +1 -1
  21. package/dist/br/pix.js +72 -62
  22. package/dist/br/pix.js.map +1 -1
  23. package/dist/br.d.ts +12 -4
  24. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  25. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  26. package/dist/components/Chat/ChatAttachments.js +46 -38
  27. package/dist/components/Chat/ChatAttachments.js.map +1 -1
  28. package/dist/components/Markdown/markdown-parse.cjs +7 -5
  29. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  30. package/dist/components/Markdown/markdown-parse.js +151 -87
  31. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  32. package/dist/components/Toast/Toast.module.cjs +1 -1
  33. package/dist/components/Toast/Toast.module.cjs.map +1 -1
  34. package/dist/components/Toast/Toast.module.js +7 -3
  35. package/dist/components/Toast/Toast.module.js.map +1 -1
  36. package/dist/components/Toast/ToastProvider.cjs +1 -1
  37. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  38. package/dist/components/Toast/ToastProvider.js +77 -51
  39. package/dist/components/Toast/ToastProvider.js.map +1 -1
  40. package/dist/data/create-data-provider.cjs +1 -1
  41. package/dist/data/create-data-provider.cjs.map +1 -1
  42. package/dist/data/create-data-provider.js +32 -21
  43. package/dist/data/create-data-provider.js.map +1 -1
  44. package/dist/http/api-client.cjs +1 -1
  45. package/dist/http/api-client.cjs.map +1 -1
  46. package/dist/http/api-client.js +81 -73
  47. package/dist/http/api-client.js.map +1 -1
  48. package/dist/http/errors.cjs +1 -1
  49. package/dist/http/errors.cjs.map +1 -1
  50. package/dist/http/errors.js +39 -35
  51. package/dist/http/errors.js.map +1 -1
  52. package/dist/neural.cjs +2 -0
  53. package/dist/neural.cjs.map +1 -0
  54. package/dist/neural.d.ts +110 -0
  55. package/dist/neural.js +48 -0
  56. package/dist/neural.js.map +1 -0
  57. package/dist/oauth/callback.cjs +2 -0
  58. package/dist/oauth/callback.cjs.map +1 -0
  59. package/dist/oauth/callback.js +42 -0
  60. package/dist/oauth/callback.js.map +1 -0
  61. package/dist/oauth/use-oauth-callback.cjs +1 -1
  62. package/dist/oauth/use-oauth-callback.cjs.map +1 -1
  63. package/dist/oauth/use-oauth-callback.js +13 -20
  64. package/dist/oauth/use-oauth-callback.js.map +1 -1
  65. package/dist/push/use-push-subscription.cjs +1 -1
  66. package/dist/push/use-push-subscription.cjs.map +1 -1
  67. package/dist/push/use-push-subscription.js +42 -42
  68. package/dist/push/use-push-subscription.js.map +1 -1
  69. package/dist/push/web-push-client.cjs +1 -1
  70. package/dist/push/web-push-client.cjs.map +1 -1
  71. package/dist/push/web-push-client.js +23 -3
  72. package/dist/push/web-push-client.js.map +1 -1
  73. package/dist/query/offline-persistence.cjs +1 -1
  74. package/dist/query/offline-persistence.cjs.map +1 -1
  75. package/dist/query/offline-persistence.js +68 -23
  76. package/dist/query/offline-persistence.js.map +1 -1
  77. package/dist/styles/component/Toast.css +6 -1
  78. package/dist/styles/feedback.css +6 -1
  79. package/dist/styles.css +1 -1
  80. package/dist/sw/background-sync.cjs +1 -1
  81. package/dist/sw/background-sync.cjs.map +1 -1
  82. package/dist/sw/background-sync.js +271 -60
  83. package/dist/sw/background-sync.js.map +1 -1
  84. package/dist/sw/cache-inspect.cjs.map +1 -1
  85. package/dist/sw/cache-inspect.js.map +1 -1
  86. package/dist/sw/cache.cjs +1 -1
  87. package/dist/sw/cache.cjs.map +1 -1
  88. package/dist/sw/cache.js +91 -64
  89. package/dist/sw/cache.js.map +1 -1
  90. package/dist/sw/credential-headers.cjs +2 -0
  91. package/dist/sw/credential-headers.cjs.map +1 -0
  92. package/dist/sw/credential-headers.js +17 -0
  93. package/dist/sw/credential-headers.js.map +1 -0
  94. package/dist/sw/session-data.cjs +2 -0
  95. package/dist/sw/session-data.cjs.map +1 -0
  96. package/dist/sw/session-data.js +17 -0
  97. package/dist/sw/session-data.js.map +1 -0
  98. package/dist/sw.cjs +1 -1
  99. package/dist/sw.d.ts +271 -23
  100. package/dist/sw.js +7 -5
  101. package/dist/telemetry/app-error-owner.cjs +2 -0
  102. package/dist/telemetry/app-error-owner.cjs.map +1 -0
  103. package/dist/telemetry/app-error-owner.js +18 -0
  104. package/dist/telemetry/app-error-owner.js.map +1 -0
  105. package/dist/telemetry/app-error-queue.cjs +1 -1
  106. package/dist/telemetry/app-error-queue.cjs.map +1 -1
  107. package/dist/telemetry/app-error-queue.js +19 -14
  108. package/dist/telemetry/app-error-queue.js.map +1 -1
  109. package/dist/telemetry/app-error-reporter.cjs +1 -1
  110. package/dist/telemetry/app-error-reporter.cjs.map +1 -1
  111. package/dist/telemetry/app-error-reporter.js +75 -34
  112. package/dist/telemetry/app-error-reporter.js.map +1 -1
  113. package/dist/telemetry/app-error-transport.cjs +1 -1
  114. package/dist/telemetry/app-error-transport.cjs.map +1 -1
  115. package/dist/telemetry/app-error-transport.js +16 -3
  116. package/dist/telemetry/app-error-transport.js.map +1 -1
  117. package/dist/tempest-react-sdk.cjs +1 -1
  118. package/dist/tempest-react-sdk.d.ts +979 -62
  119. package/dist/tempest-react-sdk.js +160 -156
  120. package/dist/utils/csv.cjs +3 -3
  121. package/dist/utils/csv.cjs.map +1 -1
  122. package/dist/utils/csv.js +16 -13
  123. package/dist/utils/csv.js.map +1 -1
  124. package/dist/utils/safe-url.cjs +2 -0
  125. package/dist/utils/safe-url.cjs.map +1 -0
  126. package/dist/utils/safe-url.js +39 -0
  127. package/dist/utils/safe-url.js.map +1 -0
  128. package/dist/vite/tempest-csp.cjs +2 -2
  129. package/dist/vite/tempest-csp.cjs.map +1 -1
  130. package/dist/vite/tempest-csp.js +118 -63
  131. package/dist/vite/tempest-csp.js.map +1 -1
  132. package/dist/vite.d.ts +21 -5
  133. package/dist/webrtc/peer-link.cjs +1 -1
  134. package/dist/webrtc/peer-link.cjs.map +1 -1
  135. package/dist/webrtc/peer-link.js +2 -10
  136. package/dist/webrtc/peer-link.js.map +1 -1
  137. package/dist/webrtc/peer-mesh.cjs +1 -1
  138. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  139. package/dist/webrtc/peer-mesh.js +110 -72
  140. package/dist/webrtc/peer-mesh.js.map +1 -1
  141. package/package.json +11 -1
  142. package/template-pwa/src/pwa.tsx +22 -0
  143. package/template-pwa/src/sw.ts +27 -14
  144. package/dist/components/Markdown/markdown-url.cjs +0 -2
  145. package/dist/components/Markdown/markdown-url.cjs.map +0 -1
  146. package/dist/components/Markdown/markdown-url.js +0 -28
  147. package/dist/components/Markdown/markdown-url.js.map +0 -1
@@ -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/cache.cjs"),r=require("./sw/background-sync.cjs"),i=require("./sw/periodic-sync.cjs"),a=require("./sw/cache-inspect.cjs");exports.clearCaches=a.clearCaches,exports.createPartialResponse=n.createPartialResponse,exports.inspectCaches=a.inspectCaches,exports.installBackgroundSync=r.installBackgroundSync,exports.installNotificationClickHandler=t.installNotificationClickHandler,exports.installPrecache=n.installPrecache,exports.installPushHandler=t.installPushHandler,exports.installRuntimeCache=n.installRuntimeCache,exports.installSkipWaitingListener=t.installSkipWaitingListener,exports.registerPeriodicSync=i.registerPeriodicSync,exports.registerServiceWorker=e.registerServiceWorker,exports.skipWaiting=e.skipWaiting,exports.unregisterAllServiceWorkers=e.unregisterAllServiceWorkers;
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
- * (e.g. on logout). All guard `caches` so they no-op safely under SSR / older
8
- * browsers.
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 every SDK-managed cache on logout
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`. Returns the original response when there is no usable
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 registered; the original fetch still
64
- * rejects (so your app can show an offline state), and the request is replayed
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 { createPartialResponse as o, installPrecache as s, installRuntimeCache as c } from "./sw/cache.js";
4
- import { installBackgroundSync as l } from "./sw/background-sync.js";
5
- import { registerPeriodicSync as u } from "./sw/periodic-sync.js";
6
- import { clearCaches as d, inspectCaches as f } from "./sw/cache-inspect.js";
7
- export { d as clearCaches, o as createPartialResponse, f as inspectCaches, l as installBackgroundSync, r as installNotificationClickHandler, s as installPrecache, i as installPushHandler, c as installRuntimeCache, a as installSkipWaitingListener, u as registerPeriodicSync, e as registerServiceWorker, t as skipWaiting, n as unregisterAllServiceWorkers };
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"}