tempest-react-sdk 0.81.0 → 0.82.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 (179) hide show
  1. package/README.md +5 -6
  2. package/bin/create-tempest-app.e2e.test.mjs +31 -0
  3. package/bin/create-tempest-app.mjs +11 -1
  4. package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
  5. package/bin/lib/doctor/vitest.mjs +120 -0
  6. package/bin/lib/doctor/vitest.test.mjs +139 -0
  7. package/bin/lib/fs/contain.mjs +7 -0
  8. package/bin/lib/openapi/escape.mjs +68 -4
  9. package/bin/lib/openapi/generate.hostile.test.mjs +137 -0
  10. package/bin/lib/openapi/generate.mjs +78 -13
  11. package/bin/lib/openapi/pattern-risk.mjs +134 -0
  12. package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
  13. package/bin/tempest-gen.test.mjs +100 -0
  14. package/bin/tempest.mjs +24 -8
  15. package/dist/access/use-can.cjs +1 -1
  16. package/dist/access/use-can.cjs.map +1 -1
  17. package/dist/access/use-can.js +25 -28
  18. package/dist/access/use-can.js.map +1 -1
  19. package/dist/auth/create-tempest-auth.cjs +1 -1
  20. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  21. package/dist/auth/create-tempest-auth.js +192 -149
  22. package/dist/auth/create-tempest-auth.js.map +1 -1
  23. package/dist/auth/session-identity.cjs +2 -0
  24. package/dist/auth/session-identity.cjs.map +1 -0
  25. package/dist/auth/session-identity.js +12 -0
  26. package/dist/auth/session-identity.js.map +1 -0
  27. package/dist/br/boleto.cjs.map +1 -1
  28. package/dist/br/boleto.js.map +1 -1
  29. package/dist/components/AuthForm/AuthLinks.cjs +1 -1
  30. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
  31. package/dist/components/AuthForm/AuthLinks.js +18 -15
  32. package/dist/components/AuthForm/AuthLinks.js.map +1 -1
  33. package/dist/components/AuthPage/AuthPage.cjs +1 -1
  34. package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
  35. package/dist/components/AuthPage/AuthPage.js +58 -52
  36. package/dist/components/AuthPage/AuthPage.js.map +1 -1
  37. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
  38. package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
  39. package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
  40. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  41. package/dist/components/Button/Button.cjs +1 -1
  42. package/dist/components/Button/Button.cjs.map +1 -1
  43. package/dist/components/Button/Button.js +41 -40
  44. package/dist/components/Button/Button.js.map +1 -1
  45. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  46. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  47. package/dist/components/Chat/ChatAttachments.js +38 -38
  48. package/dist/components/Form/Form.cjs +1 -1
  49. package/dist/components/Form/Form.cjs.map +1 -1
  50. package/dist/components/Form/Form.js +30 -32
  51. package/dist/components/Form/Form.js.map +1 -1
  52. package/dist/components/Layout/Layout.cjs +1 -1
  53. package/dist/components/Layout/Layout.cjs.map +1 -1
  54. package/dist/components/Layout/Layout.js +25 -24
  55. package/dist/components/Layout/Layout.js.map +1 -1
  56. package/dist/components/Layout/gap.cjs +2 -0
  57. package/dist/components/Layout/gap.cjs.map +1 -0
  58. package/dist/components/Layout/gap.js +15 -0
  59. package/dist/components/Layout/gap.js.map +1 -0
  60. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  61. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  62. package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
  63. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  64. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  65. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  66. package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
  67. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
  68. package/dist/components/Sidebar/SidebarEntry.js +30 -29
  69. package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
  70. package/dist/data/create-data-provider.cjs +1 -1
  71. package/dist/data/create-data-provider.cjs.map +1 -1
  72. package/dist/data/create-data-provider.js +1 -2
  73. package/dist/data/create-data-provider.js.map +1 -1
  74. package/dist/forms/validate-form.cjs +1 -1
  75. package/dist/forms/validate-form.cjs.map +1 -1
  76. package/dist/forms/validate-form.js +6 -1
  77. package/dist/forms/validate-form.js.map +1 -1
  78. package/dist/forms/zod-resolver.cjs +1 -1
  79. package/dist/forms/zod-resolver.cjs.map +1 -1
  80. package/dist/forms/zod-resolver.js +8 -3
  81. package/dist/forms/zod-resolver.js.map +1 -1
  82. package/dist/http/api-client.cjs +1 -1
  83. package/dist/http/api-client.cjs.map +1 -1
  84. package/dist/http/api-client.js +90 -72
  85. package/dist/http/api-client.js.map +1 -1
  86. package/dist/http/credential-scope.cjs +1 -1
  87. package/dist/http/credential-scope.cjs.map +1 -1
  88. package/dist/http/credential-scope.js +33 -9
  89. package/dist/http/credential-scope.js.map +1 -1
  90. package/dist/http/presigned-post.cjs.map +1 -1
  91. package/dist/http/presigned-post.js.map +1 -1
  92. package/dist/http/resumable-upload.cjs +1 -1
  93. package/dist/http/resumable-upload.cjs.map +1 -1
  94. package/dist/http/resumable-upload.js +90 -84
  95. package/dist/http/resumable-upload.js.map +1 -1
  96. package/dist/http/upload-with-progress.cjs +1 -1
  97. package/dist/http/upload-with-progress.cjs.map +1 -1
  98. package/dist/http/upload-with-progress.js +8 -8
  99. package/dist/http/upload-with-progress.js.map +1 -1
  100. package/dist/oauth/GoogleSignIn.cjs +1 -1
  101. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  102. package/dist/oauth/GoogleSignIn.js +5 -4
  103. package/dist/oauth/GoogleSignIn.js.map +1 -1
  104. package/dist/offline/create-offline-sync.cjs +1 -1
  105. package/dist/offline/create-offline-sync.cjs.map +1 -1
  106. package/dist/offline/create-offline-sync.js +87 -69
  107. package/dist/offline/create-offline-sync.js.map +1 -1
  108. package/dist/query/offline-persistence.cjs +1 -1
  109. package/dist/query/offline-persistence.cjs.map +1 -1
  110. package/dist/query/offline-persistence.js +54 -44
  111. package/dist/query/offline-persistence.js.map +1 -1
  112. package/dist/sse/create-event-stream.cjs +1 -1
  113. package/dist/sse/create-event-stream.cjs.map +1 -1
  114. package/dist/sse/create-event-stream.js +48 -41
  115. package/dist/sse/create-event-stream.js.map +1 -1
  116. package/dist/sse/ticket-event-stream.cjs +1 -1
  117. package/dist/sse/ticket-event-stream.cjs.map +1 -1
  118. package/dist/sse/ticket-event-stream.js +48 -19
  119. package/dist/sse/ticket-event-stream.js.map +1 -1
  120. package/dist/sse/use-event-stream.cjs +1 -1
  121. package/dist/sse/use-event-stream.cjs.map +1 -1
  122. package/dist/sse/use-event-stream.js +19 -14
  123. package/dist/sse/use-event-stream.js.map +1 -1
  124. package/dist/sw/background-sync.cjs +1 -1
  125. package/dist/sw/background-sync.cjs.map +1 -1
  126. package/dist/sw/background-sync.js +10 -8
  127. package/dist/sw/background-sync.js.map +1 -1
  128. package/dist/sw/cache.cjs +1 -1
  129. package/dist/sw/cache.cjs.map +1 -1
  130. package/dist/sw/cache.js +36 -30
  131. package/dist/sw/cache.js.map +1 -1
  132. package/dist/sw/create-push-handler.cjs +1 -1
  133. package/dist/sw/create-push-handler.cjs.map +1 -1
  134. package/dist/sw/create-push-handler.js +20 -10
  135. package/dist/sw/create-push-handler.js.map +1 -1
  136. package/dist/sw.d.ts +44 -13
  137. package/dist/tabular/compact.cjs +1 -1
  138. package/dist/tabular/compact.cjs.map +1 -1
  139. package/dist/tabular/compact.js +35 -16
  140. package/dist/tabular/compact.js.map +1 -1
  141. package/dist/tabular/manifest.cjs +1 -1
  142. package/dist/tabular/manifest.cjs.map +1 -1
  143. package/dist/tabular/manifest.js +33 -15
  144. package/dist/tabular/manifest.js.map +1 -1
  145. package/dist/tabular.d.ts +11 -1
  146. package/dist/tempest-react-sdk.cjs +1 -1
  147. package/dist/tempest-react-sdk.d.ts +457 -68
  148. package/dist/tempest-react-sdk.js +224 -224
  149. package/dist/theme/initial-theme.cjs +4 -4
  150. package/dist/theme/initial-theme.cjs.map +1 -1
  151. package/dist/theme/initial-theme.js +8 -5
  152. package/dist/theme/initial-theme.js.map +1 -1
  153. package/dist/utils/compressed-storage.cjs +1 -1
  154. package/dist/utils/compressed-storage.cjs.map +1 -1
  155. package/dist/utils/compressed-storage.js +13 -7
  156. package/dist/utils/compressed-storage.js.map +1 -1
  157. package/dist/utils/objects.cjs +1 -1
  158. package/dist/utils/objects.cjs.map +1 -1
  159. package/dist/utils/objects.js +13 -7
  160. package/dist/utils/objects.js.map +1 -1
  161. package/dist/utils/safe-url.cjs +1 -1
  162. package/dist/utils/safe-url.cjs.map +1 -1
  163. package/dist/utils/safe-url.js +4 -1
  164. package/dist/utils/safe-url.js.map +1 -1
  165. package/dist/webrtc/peer-mesh.cjs +1 -1
  166. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  167. package/dist/webrtc/peer-mesh.js +7 -1
  168. package/dist/webrtc/peer-mesh.js.map +1 -1
  169. package/dist/ws/create-web-socket.cjs +1 -1
  170. package/dist/ws/create-web-socket.cjs.map +1 -1
  171. package/dist/ws/create-web-socket.js +77 -61
  172. package/dist/ws/create-web-socket.js.map +1 -1
  173. package/dist/ws/use-web-socket.cjs +1 -1
  174. package/dist/ws/use-web-socket.cjs.map +1 -1
  175. package/dist/ws/use-web-socket.js +4 -3
  176. package/dist/ws/use-web-socket.js.map +1 -1
  177. package/package.json +1 -1
  178. package/template-pwa/src/pwa.tsx +42 -0
  179. package/template-pwa/src/sw.ts +5 -4
package/dist/sw/cache.js CHANGED
@@ -12,43 +12,49 @@ function a(e, t) {
12
12
  function o(e) {
13
13
  return (e.headers.get("cache-control") ?? "").toLowerCase().split(",").map((e) => e.trim().split("=")[0]);
14
14
  }
15
- function s(e, t) {
16
- if (!e.ok) return !1;
15
+ function s(e) {
16
+ return (e ?? "").toLowerCase().split(",").map((e) => e.trim());
17
+ }
18
+ function c(e, t) {
19
+ if (!e.ok || s(e.headers.get("vary")).includes("*")) return !1;
17
20
  let n = o(e);
18
21
  return n.includes("no-store") ? !1 : t.cacheAuthenticated === !0 || !n.includes("private");
19
22
  }
20
- function c(t, n) {
23
+ function l(e, t) {
24
+ return e.mode === "cors" && e.credentials === "include" || u(e, t).length > 0;
25
+ }
26
+ function u(t, n) {
21
27
  return [...e(n.credentialHeaders)].filter((e) => t.headers.has(e));
22
28
  }
23
- async function l(e, t, n, r) {
24
- if (!s(n, r)) return;
29
+ async function d(e, t, n, r) {
30
+ if (!c(n, r)) return;
25
31
  let a = new Headers(n.headers);
26
32
  if (a.set(i, String(Date.now())), r.cacheAuthenticated) {
27
- let e = a.get("vary"), n = (e ?? "").toLowerCase().split(",").map((e) => e.trim()), i = c(t, r).filter((e) => !n.includes(e));
28
- i.length > 0 && !n.includes("*") && a.set("vary", [e, ...i].filter(Boolean).join(", "));
33
+ let e = a.get("vary"), n = s(e), i = u(t, r).filter((e) => !n.includes(e));
34
+ i.length > 0 && a.set("vary", [e, ...i].filter(Boolean).join(", "));
29
35
  }
30
36
  let o = n.clone();
31
37
  await e.put(t, new Response(o.body, {
32
38
  status: o.status,
33
39
  statusText: o.statusText,
34
40
  headers: a
35
- })), await u(r.cacheName, r.maxEntries);
41
+ })), await f(r.cacheName, r.maxEntries);
36
42
  }
37
- async function u(e, t) {
43
+ async function f(e, t) {
38
44
  if (!t) return;
39
45
  let n = await caches.open(e), r = await n.keys();
40
46
  if (!(r.length <= t)) for (let e of r.slice(0, r.length - t)) await n.delete(e);
41
47
  }
42
- async function d(e, t) {
48
+ async function p(e, t) {
43
49
  let n = await caches.open(t.cacheName), r = await n.match(e);
44
50
  if (r && !a(r, t.maxAgeSeconds)) return r;
45
51
  let i = await fetch(e);
46
- return await l(n, e, i, t), i;
52
+ return await d(n, e, i, t), i;
47
53
  }
48
- async function f(e, t, n) {
54
+ async function m(e, t, n) {
49
55
  let r = await caches.open(t.cacheName), i = (async () => {
50
56
  let n = await fetch(e);
51
- return await l(r, e, n, t), n;
57
+ return await d(r, e, n, t), n;
52
58
  })(), o = (t.networkTimeoutSeconds ?? 0) * 1e3;
53
59
  try {
54
60
  if (o > 0) {
@@ -64,24 +70,24 @@ async function f(e, t, n) {
64
70
  throw Error("network-first: no network and no cache");
65
71
  }
66
72
  }
67
- async function p(e, t, n) {
68
- let r = await caches.open(t.cacheName), i = await r.match(e), o = fetch(e).then(async (n) => (await l(r, e, n, t), n)).catch(() => void 0), s = i && !a(i, t.maxAgeSeconds) ? i : void 0;
73
+ async function h(e, t, n) {
74
+ let r = await caches.open(t.cacheName), i = await r.match(e), o = fetch(e).then(async (n) => (await d(r, e, n, t), n)).catch(() => void 0), s = i && !a(i, t.maxAgeSeconds) ? i : void 0;
69
75
  if (s) return n(o), s;
70
76
  let c = await o;
71
77
  if (c) return c;
72
78
  throw Error("stale-while-revalidate: no network and no cache");
73
79
  }
74
- function m(e, t, n) {
80
+ function g(e, t, n) {
75
81
  switch (t.strategy) {
76
- case "cache-first": return d(e, t);
77
- case "network-first": return f(e, t, n);
78
- case "stale-while-revalidate": return p(e, t, n);
82
+ case "cache-first": return p(e, t);
83
+ case "network-first": return m(e, t, n);
84
+ case "stale-while-revalidate": return h(e, t, n);
79
85
  }
80
86
  }
81
- function h(e, t, n) {
87
+ function _(e, t, n) {
82
88
  return typeof e.match == "function" ? e.match(t, n) : e.match.test(t.href);
83
89
  }
84
- async function g(e, t) {
90
+ async function v(e, t) {
85
91
  let n = e.headers.get("range");
86
92
  if (!n) return t;
87
93
  let r = /^bytes=(\d*)-(\d*)$/.exec(n.trim());
@@ -106,28 +112,28 @@ async function g(e, t) {
106
112
  headers: l
107
113
  });
108
114
  }
109
- function _(e) {
115
+ function y(e) {
110
116
  t().addEventListener("fetch", (t) => {
111
117
  let n = t.request;
112
118
  if (n.method !== "GET") return;
113
- let r = new URL(n.url), i = e.find((e) => h(e, r, n));
114
- if (!i || !i.cacheAuthenticated && c(n, i).length > 0) return;
119
+ let r = new URL(n.url), i = e.find((e) => _(e, r, n));
120
+ if (!i || !i.cacheAuthenticated && l(n, i)) return;
115
121
  let a = (e) => t.waitUntil(e);
116
122
  if (i.rangeRequests && n.headers.has("range")) {
117
123
  let e = new Request(n.url, {
118
- headers: v(n.headers),
124
+ headers: b(n.headers),
119
125
  credentials: n.credentials,
120
126
  mode: n.mode === "navigate" ? "same-origin" : n.mode
121
127
  });
122
- t.respondWith(m(e, i, a).then((e) => g(n, e)));
123
- } else t.respondWith(m(n, i, a));
128
+ t.respondWith(g(e, i, a).then((e) => v(n, e)));
129
+ } else t.respondWith(g(n, i, a));
124
130
  });
125
131
  }
126
- function v(e) {
132
+ function b(e) {
127
133
  let t = new Headers(e);
128
134
  return t.delete("range"), t;
129
135
  }
130
- function y(e = {}) {
136
+ function x(e = {}) {
131
137
  let i = t(), { manifestUrl: a = "/precache-manifest.json", cacheName: o = "tempest-precache", navigateFallback: s = "/index.html", navigateFallbackDenylist: c = [], skipWaiting: l = !0, navigationPreload: u = !0 } = e;
132
138
  i.addEventListener("install", (e) => {
133
139
  e.waitUntil((async () => {
@@ -162,6 +168,6 @@ function y(e = {}) {
162
168
  });
163
169
  }
164
170
  //#endregion
165
- export { g as createPartialResponse, y as installPrecache, _ as installRuntimeCache };
171
+ export { v as createPartialResponse, x as installPrecache, y as installRuntimeCache };
166
172
 
167
173
  //# sourceMappingURL=cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","names":["getSwScope","precacheName","precachedPaths","CACHED_AT_HEADER","isExpired","maxAgeSeconds","stamp","response","dateHeader","storedAt","cacheControlDirectives","directive","isStorable","directives","route","credentialsOn","credentialHeaderSet","request","name","store","headers","vary","varies","missing","copy","cache","trimCache","maxEntries","cacheName","keys","key","cacheFirst","cached","networkFirst","network","timeoutMs","timeout","extend","reject","staleWhileRevalidate","fresh","runRoute","routeMatches","url","createPartialResponse","rangeHeader","match","buffer","total","start","end","suffix","slice","installRuntimeCache","event","routes","candidate","promise","full","stripRange","out","installPrecache","sw","options","manifest","manifestUrl","skipWaiting","navigationPreload","navigateFallbackDenylist","re","shell","navigateFallback"],"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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"}
1
+ {"version":3,"file":"cache.js","names":["getSwScope","precacheName","precachedPaths","CACHED_AT_HEADER","isExpired","maxAgeSeconds","stamp","response","dateHeader","storedAt","cacheControlDirectives","directive","varyNames","vary","name","isStorable","directives","route","isCredentialed","request","credentialsOn","credentialHeaderSet","store","headers","varies","missing","copy","cache","trimCache","maxEntries","cacheName","keys","key","cacheFirst","cached","networkFirst","network","timeoutMs","timeout","extend","reject","staleWhileRevalidate","fresh","runRoute","routeMatches","url","createPartialResponse","rangeHeader","match","buffer","total","start","end","suffix","slice","installRuntimeCache","event","routes","candidate","promise","full","stripRange","out","installPrecache","sw","options","manifest","manifestUrl","skipWaiting","navigationPreload","navigateFallbackDenylist","re","shell","navigateFallback"],"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 credentialed requests — those that carry a credential\n * header (see `credentialHeaders`) or are a CORS-mode `fetch` sent with\n * `credentials: \"include\"` — 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 only partly visible here: the worker cannot see\n * `Cookie`. What it can see is a `fetch` that asked for cookies explicitly —\n * `mode: \"cors\"` with `credentials: \"include\"`, which is what `ApiClient`\n * sends under `withCredentials` — and since 0.82.0 that request is treated\n * as credentialed too (#531). `<img>`, `<audio>` and `<video>` also send\n * `credentials: \"include\"`, but in `no-cors` mode, so they keep caching. A\n * plain same-origin `fetch` (`credentials: \"same-origin\"`, the default)\n * carries the cookie without saying so; such an API stays private only\n * through `Cache-Control: private`/`no-store` on its responses and\n * `clearSessionData()` on logout. Opting a cookie request in caches it by\n * URL — there is no credential header to vary on — so `clearSessionData()`\n * is then its only isolation.\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/** The comma-separated names of a response's `Vary` header, trimmed and lower-cased. */\nfunction varyNames(vary: string | null): string[] {\n return (vary ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((name) => name.trim());\n}\n\n/**\n * Whether a network response may be written to the route's cache: it must be\n * `ok`, never `no-store`, never `Vary: *`, and `private` only under\n * `cacheAuthenticated`.\n *\n * `Vary: *` says no later request can be proven to match, and `Cache.put`\n * rejects it with a `TypeError`. Before 0.82.0 that rejection escaped the\n * strategy, so a response the network had delivered became the offline\n * fallback path (network-first), an error (cache-first) or nothing at all\n * (stale-while-revalidate) (#534).\n */\nfunction isStorable(response: Response, route: RuntimeRoute): boolean {\n if (!response.ok) return false;\n if (varyNames(response.headers.get(\"vary\")).includes(\"*\")) 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/**\n * Whether a request is session-bound for its route: it carries a credential\n * header, or it is a CORS `fetch` that asked for cookies with\n * `credentials: \"include\"` (`ApiClient` under `withCredentials`). A `no-cors`\n * request — `<img>`, `<audio>`, `<video>` — also sends `include` by default and\n * is not counted, or every image would bypass the cache.\n */\nfunction isCredentialed(request: Request, route: RuntimeRoute): boolean {\n if (request.mode === \"cors\" && request.credentials === \"include\") return true;\n return credentialsOn(request, route).length > 0;\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 = varyNames(vary);\n const missing = credentialsOn(request, route).filter((name) => !varies.includes(name));\n if (missing.length > 0) {\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`), or a CORS `fetch` sent with\n * `credentials: \"include\"`, 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) or `Vary: *` 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 && isCredentialed(request, route)) 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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"}
@@ -1,2 +1,2 @@
1
- function e(){return globalThis}var t=[`body`,`image`,`tag`,`requireInteraction`,`vibrate`,`silent`,`renotify`,`timestamp`,`dir`,`lang`];function n(e,n,r){let i={icon:e.icon??n,badge:e.badge??r};for(let n of t)e[n]!==void 0&&(i[n]=e[n]);let a={url:e.url??`/`,...e.data??{}};if(e.actions){let t={};i.actions=e.actions.map(({url:e,...n})=>(e!==void 0&&(t[n.action]=e),n)),Object.keys(t).length>0&&(a.actionUrls=t)}return i.data=a,i}function r(t={}){let r=e(),{defaultTitle:i=`Notificação`,defaultIcon:a,defaultBadge:o,transform:s}=t;r.addEventListener(`push`,e=>{if(!e.data)return;let t;try{t=e.data.json()}catch{t={title:i,body:e.data.text()}}let c=s?s(t):t;if(!c)return;let l=c.title??i,u=n(c,a,o);e.waitUntil(r.registration.showNotification(l,u))})}function i(e,t){if(typeof e==`string`)return e;if(!e||typeof e!=`object`)return`/`;let n=e;if(t&&n.actionUrls&&typeof n.actionUrls==`object`){let e=n.actionUrls[t];if(typeof e==`string`)return e}return typeof n.url==`string`?n.url:`/`}function a(e,t){try{return new URL(t,e).href===new URL(e).href}catch{return!1}}function o(t={}){let n=e(),r=t.resolveUrl??i;n.addEventListener(`notificationclick`,e=>{e.notification.close();let t=r(e.notification.data,e.action);e.waitUntil((async()=>{let e=await n.clients.matchAll({type:`window`,includeUncontrolled:!0});for(let n of e)if(a(n.url,t))return n.focus();return n.clients.openWindow(t)})())})}function s(){let t=e();t.addEventListener(`message`,e=>{e.data?.type===`SKIP_WAITING`&&t.skipWaiting()})}exports.installNotificationClickHandler=o,exports.installPushHandler=r,exports.installSkipWaitingListener=s;
1
+ function e(){return globalThis}var t=[`body`,`image`,`tag`,`requireInteraction`,`vibrate`,`silent`,`renotify`,`timestamp`,`dir`,`lang`];function n(e,n,r){let i={icon:e.icon??n,badge:e.badge??r};for(let n of t)e[n]!==void 0&&(i[n]=e[n]);let a={...e.data??{},url:e.url??`/`};if(delete a.actionUrls,e.actions){let t={};i.actions=e.actions.map(({url:e,...n})=>(e!==void 0&&(t[n.action]=e),n)),Object.keys(t).length>0&&(a.actionUrls=t)}return i.data=a,i}function r(t={}){let r=e(),{defaultTitle:i=`Notificação`,defaultIcon:a,defaultBadge:o,transform:s}=t;r.addEventListener(`push`,e=>{if(!e.data)return;let t;try{t=e.data.json()}catch{t={title:i,body:e.data.text()}}let c=s?s(t):t;if(!c)return;let l=c.title??i,u=n(c,a,o);e.waitUntil(r.registration.showNotification(l,u))})}function i(e,t){if(typeof e==`string`)return e;if(!e||typeof e!=`object`)return`/`;let n=e;if(t&&n.actionUrls&&typeof n.actionUrls==`object`){let e=n.actionUrls[t];if(typeof e==`string`)return e}return typeof n.url==`string`?n.url:`/`}function a(e,t,n){if(t===void 0)return`/`;let r;try{r=new URL(e,t)}catch{return`/`}return r.protocol!==`http:`&&r.protocol!==`https:`||n&&r.origin!==t?`/`:e}function o(e,t){try{return new URL(t,e).href===new URL(e).href}catch{return!1}}function s(t={}){let n=e(),r=t.resolveUrl??i,s=t.resolveUrl===void 0;n.addEventListener(`notificationclick`,e=>{e.notification.close();let t=a(r(e.notification.data,e.action),n.location?.origin,s);e.waitUntil((async()=>{let e=await n.clients.matchAll({type:`window`,includeUncontrolled:!0});for(let n of e)if(o(n.url,t))return n.focus();return n.clients.openWindow(t)})())})}function c(){let t=e();t.addEventListener(`message`,e=>{e.data?.type===`SKIP_WAITING`&&t.skipWaiting()})}exports.installNotificationClickHandler=s,exports.installPushHandler=r,exports.installSkipWaitingListener=c;
2
2
  //# sourceMappingURL=create-push-handler.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-push-handler.cjs","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","clientShowsTarget","target","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /** Arbitrary extra data forwarded to `event.notification.data`. */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { url: payload.url ?? \"/\", ...(payload.data ?? {}) };\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the top-level `url` (default `\"/\"`), the payload's\n * own `data`, and `actionUrls` mapping each action id to its `url`.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = resolveUrl(event.notification.data, event.action);\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-push-handler.cjs","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","allowedTarget","origin","target","sameOriginOnly","clientShowsTarget","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n location?: { origin: string };\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /**\n * Arbitrary extra data forwarded to `event.notification.data`.\n *\n * Its `url` and `actionUrls` keys are overwritten by the top-level `url` and\n * the actions' `url`s: those two keys decide where a click goes, and a\n * free-form bag — often filled from the record that triggered the push — must\n * not be able to redirect it.\n */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * `payload.data` is spread **first**, so its own `url` or `actionUrls` can never\n * replace the destinations the payload declares: until 0.81.0 it was spread last,\n * and `{ url: \"/orders/9\", data: { url: \"https://elsewhere\" } }` opened\n * `https://elsewhere` on click.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { ...(payload.data ?? {}), url: payload.url ?? \"/\" };\n delete data.actionUrls;\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the payload's own `data`, then the top-level `url`\n * (default `\"/\"`) and `actionUrls` mapping each action id to its `url` — in that\n * order, so `payload.data` cannot override where a click goes.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n *\n * The default only ever opens a page of the worker's **own origin**: a URL on\n * another origin, or with a scheme other than `http:`/`https:`, becomes `\"/\"`.\n * A resolver of your own is how an app opts into another origin — what it\n * returns may point anywhere `http:`/`https:`, and any other scheme still\n * becomes `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * The URL a click may open, given the worker's origin.\n *\n * The push payload is written by the app's server, but its `url` is often\n * copied from the record that triggered the push — a chat message, a ticket —\n * and `openWindow` opens it with the app's notification as the only context the\n * user sees. So the default resolver keeps to the worker's own origin, the same\n * line `safeLinkUrl` draws for rendered links, and a custom resolver is the\n * explicit opt-in to leave it. Neither may open a non-`http(s)` scheme.\n *\n * The URL comes back as given, not resolved, so a relative path stays relative —\n * `openWindow` resolves it against the worker's location exactly like this check\n * did.\n *\n * @param target - What the resolver returned.\n * @param origin - The worker's origin; `undefined` outside a worker, where\n * nothing can be judged and only `\"/\"` is opened.\n * @param sameOriginOnly - Whether another origin is refused too.\n * @returns `target` when it may be opened, `\"/\"` otherwise.\n */\nfunction allowedTarget(\n target: string,\n origin: string | undefined,\n sameOriginOnly: boolean,\n): string {\n if (origin === undefined) return \"/\";\n let url: URL;\n try {\n url = new URL(target, origin);\n } catch {\n return \"/\";\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return \"/\";\n if (sameOriginOnly && url.origin !== origin) return \"/\";\n return target;\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`. Only a same-origin `http(s)` URL is\n * opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}\n * for opting into another origin.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n const sameOriginOnly = options.resolveUrl === undefined;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = allowedTarget(\n resolveUrl(event.notification.data, event.action),\n sw.location?.origin,\n sameOriginOnly,\n );\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
@@ -21,10 +21,10 @@ function n(e, n, r) {
21
21
  };
22
22
  for (let n of t) e[n] !== void 0 && (i[n] = e[n]);
23
23
  let a = {
24
- url: e.url ?? "/",
25
- ...e.data ?? {}
24
+ ...e.data ?? {},
25
+ url: e.url ?? "/"
26
26
  };
27
- if (e.actions) {
27
+ if (delete a.actionUrls, e.actions) {
28
28
  let t = {};
29
29
  i.actions = e.actions.map(({ url: e, ...n }) => (e !== void 0 && (t[n.action] = e), n)), Object.keys(t).length > 0 && (a.actionUrls = t);
30
30
  }
@@ -59,35 +59,45 @@ function i(e, t) {
59
59
  }
60
60
  return typeof n.url == "string" ? n.url : "/";
61
61
  }
62
- function a(e, t) {
62
+ function a(e, t, n) {
63
+ if (t === void 0) return "/";
64
+ let r;
65
+ try {
66
+ r = new URL(e, t);
67
+ } catch {
68
+ return "/";
69
+ }
70
+ return r.protocol !== "http:" && r.protocol !== "https:" || n && r.origin !== t ? "/" : e;
71
+ }
72
+ function o(e, t) {
63
73
  try {
64
74
  return new URL(t, e).href === new URL(e).href;
65
75
  } catch {
66
76
  return !1;
67
77
  }
68
78
  }
69
- function o(t = {}) {
70
- let n = e(), r = t.resolveUrl ?? i;
79
+ function s(t = {}) {
80
+ let n = e(), r = t.resolveUrl ?? i, s = t.resolveUrl === void 0;
71
81
  n.addEventListener("notificationclick", (e) => {
72
82
  e.notification.close();
73
- let t = r(e.notification.data, e.action);
83
+ let t = a(r(e.notification.data, e.action), n.location?.origin, s);
74
84
  e.waitUntil((async () => {
75
85
  let e = await n.clients.matchAll({
76
86
  type: "window",
77
87
  includeUncontrolled: !0
78
88
  });
79
- for (let n of e) if (a(n.url, t)) return n.focus();
89
+ for (let n of e) if (o(n.url, t)) return n.focus();
80
90
  return n.clients.openWindow(t);
81
91
  })());
82
92
  });
83
93
  }
84
- function s() {
94
+ function c() {
85
95
  let t = e();
86
96
  t.addEventListener("message", (e) => {
87
97
  e.data?.type === "SKIP_WAITING" && t.skipWaiting();
88
98
  });
89
99
  }
90
100
  //#endregion
91
- export { o as installNotificationClickHandler, r as installPushHandler, s as installSkipWaitingListener };
101
+ export { s as installNotificationClickHandler, r as installPushHandler, c as installSkipWaitingListener };
92
102
 
93
103
  //# sourceMappingURL=create-push-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-push-handler.js","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","clientShowsTarget","target","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /** Arbitrary extra data forwarded to `event.notification.data`. */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { url: payload.url ?? \"/\", ...(payload.data ?? {}) };\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the top-level `url` (default `\"/\"`), the payload's\n * own `data`, and `actionUrls` mapping each action id to its `url`.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = resolveUrl(event.notification.data, event.action);\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-push-handler.js","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","allowedTarget","origin","target","sameOriginOnly","clientShowsTarget","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n location?: { origin: string };\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /**\n * Arbitrary extra data forwarded to `event.notification.data`.\n *\n * Its `url` and `actionUrls` keys are overwritten by the top-level `url` and\n * the actions' `url`s: those two keys decide where a click goes, and a\n * free-form bag — often filled from the record that triggered the push — must\n * not be able to redirect it.\n */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * `payload.data` is spread **first**, so its own `url` or `actionUrls` can never\n * replace the destinations the payload declares: until 0.81.0 it was spread last,\n * and `{ url: \"/orders/9\", data: { url: \"https://elsewhere\" } }` opened\n * `https://elsewhere` on click.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { ...(payload.data ?? {}), url: payload.url ?? \"/\" };\n delete data.actionUrls;\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the payload's own `data`, then the top-level `url`\n * (default `\"/\"`) and `actionUrls` mapping each action id to its `url` — in that\n * order, so `payload.data` cannot override where a click goes.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n *\n * The default only ever opens a page of the worker's **own origin**: a URL on\n * another origin, or with a scheme other than `http:`/`https:`, becomes `\"/\"`.\n * A resolver of your own is how an app opts into another origin — what it\n * returns may point anywhere `http:`/`https:`, and any other scheme still\n * becomes `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * The URL a click may open, given the worker's origin.\n *\n * The push payload is written by the app's server, but its `url` is often\n * copied from the record that triggered the push — a chat message, a ticket —\n * and `openWindow` opens it with the app's notification as the only context the\n * user sees. So the default resolver keeps to the worker's own origin, the same\n * line `safeLinkUrl` draws for rendered links, and a custom resolver is the\n * explicit opt-in to leave it. Neither may open a non-`http(s)` scheme.\n *\n * The URL comes back as given, not resolved, so a relative path stays relative —\n * `openWindow` resolves it against the worker's location exactly like this check\n * did.\n *\n * @param target - What the resolver returned.\n * @param origin - The worker's origin; `undefined` outside a worker, where\n * nothing can be judged and only `\"/\"` is opened.\n * @param sameOriginOnly - Whether another origin is refused too.\n * @returns `target` when it may be opened, `\"/\"` otherwise.\n */\nfunction allowedTarget(\n target: string,\n origin: string | undefined,\n sameOriginOnly: boolean,\n): string {\n if (origin === undefined) return \"/\";\n let url: URL;\n try {\n url = new URL(target, origin);\n } catch {\n return \"/\";\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return \"/\";\n if (sameOriginOnly && url.origin !== origin) return \"/\";\n return target;\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`. Only a same-origin `http(s)` URL is\n * opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}\n * for opting into another origin.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n const sameOriginOnly = options.resolveUrl === undefined;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = allowedTarget(\n resolveUrl(event.notification.data, event.action),\n sw.location?.origin,\n sameOriginOnly,\n );\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}