tempest-react-sdk 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +2 -2
  2. package/bin/create-tempest-app.e2e.test.mjs +112 -0
  3. package/bin/create-tempest-app.mjs +69 -28
  4. package/bin/lib/alias/collect.mjs +20 -3
  5. package/bin/lib/alias/index.mjs +12 -1
  6. package/bin/lib/alias/index.test.mjs +24 -0
  7. package/bin/lib/component/generate.mjs +191 -0
  8. package/bin/lib/component/generate.test.mjs +336 -0
  9. package/bin/lib/css/analyze.test.mjs +21 -1
  10. package/bin/lib/css/collect.mjs +14 -0
  11. package/bin/lib/css/extract.mjs +63 -5
  12. package/bin/lib/css/extract.test.mjs +110 -1
  13. package/bin/lib/css/fix.e2e.test.mjs +50 -0
  14. package/bin/lib/css/index.mjs +6 -1
  15. package/bin/lib/fs/contain.mjs +50 -0
  16. package/bin/lib/icons/generate.mjs +28 -3
  17. package/bin/lib/icons/generate.test.mjs +57 -1
  18. package/bin/lib/openapi/escape.mjs +224 -0
  19. package/bin/lib/openapi/generate.encode.test.mjs +120 -0
  20. package/bin/lib/openapi/generate.hostile.test.mjs +621 -0
  21. package/bin/lib/openapi/generate.mjs +129 -18
  22. package/bin/lib/openapi/generate.test.mjs +7 -2
  23. package/bin/lib/openapi/schema-to-zod.mjs +43 -4
  24. package/bin/tempest-gen.test.mjs +188 -0
  25. package/bin/tempest.mjs +263 -6
  26. package/dist/auth/create-tempest-auth.cjs +1 -1
  27. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  28. package/dist/auth/create-tempest-auth.js +203 -58
  29. package/dist/auth/create-tempest-auth.js.map +1 -1
  30. package/dist/auth/refresh-queue.cjs +1 -1
  31. package/dist/auth/refresh-queue.cjs.map +1 -1
  32. package/dist/auth/refresh-queue.js +13 -6
  33. package/dist/auth/refresh-queue.js.map +1 -1
  34. package/dist/br/pix.cjs +1 -1
  35. package/dist/br/pix.cjs.map +1 -1
  36. package/dist/br/pix.js +85 -72
  37. package/dist/br/pix.js.map +1 -1
  38. package/dist/br.d.ts +19 -4
  39. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  40. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  41. package/dist/components/Chat/ChatAttachments.js +46 -38
  42. package/dist/components/Chat/ChatAttachments.js.map +1 -1
  43. package/dist/components/CodeBlock/CodeBlock.cjs +1 -1
  44. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  45. package/dist/components/CodeBlock/CodeBlock.js +1 -1
  46. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  47. package/dist/components/CodeBlock/tokenize.cjs +1 -1
  48. package/dist/components/CodeBlock/tokenize.cjs.map +1 -1
  49. package/dist/components/CodeBlock/tokenize.js +54 -25
  50. package/dist/components/CodeBlock/tokenize.js.map +1 -1
  51. package/dist/components/Markdown/markdown-parse.cjs +7 -5
  52. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  53. package/dist/components/Markdown/markdown-parse.js +171 -90
  54. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  55. package/dist/components/Toast/Toast.module.cjs +1 -1
  56. package/dist/components/Toast/Toast.module.cjs.map +1 -1
  57. package/dist/components/Toast/Toast.module.js +7 -3
  58. package/dist/components/Toast/Toast.module.js.map +1 -1
  59. package/dist/components/Toast/ToastProvider.cjs +1 -1
  60. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  61. package/dist/components/Toast/ToastProvider.js +77 -51
  62. package/dist/components/Toast/ToastProvider.js.map +1 -1
  63. package/dist/data/create-data-provider.cjs +1 -1
  64. package/dist/data/create-data-provider.cjs.map +1 -1
  65. package/dist/data/create-data-provider.js +32 -21
  66. package/dist/data/create-data-provider.js.map +1 -1
  67. package/dist/http/api-client.cjs +1 -1
  68. package/dist/http/api-client.cjs.map +1 -1
  69. package/dist/http/api-client.js +81 -73
  70. package/dist/http/api-client.js.map +1 -1
  71. package/dist/http/errors.cjs +1 -1
  72. package/dist/http/errors.cjs.map +1 -1
  73. package/dist/http/errors.js +39 -35
  74. package/dist/http/errors.js.map +1 -1
  75. package/dist/http/resumable-upload.cjs +1 -1
  76. package/dist/http/resumable-upload.cjs.map +1 -1
  77. package/dist/http/resumable-upload.js +141 -105
  78. package/dist/http/resumable-upload.js.map +1 -1
  79. package/dist/http/retry.cjs +1 -1
  80. package/dist/http/retry.cjs.map +1 -1
  81. package/dist/http/retry.js +5 -6
  82. package/dist/http/retry.js.map +1 -1
  83. package/dist/oauth/callback.cjs +2 -0
  84. package/dist/oauth/callback.cjs.map +1 -0
  85. package/dist/oauth/callback.js +42 -0
  86. package/dist/oauth/callback.js.map +1 -0
  87. package/dist/oauth/use-oauth-callback.cjs +1 -1
  88. package/dist/oauth/use-oauth-callback.cjs.map +1 -1
  89. package/dist/oauth/use-oauth-callback.js +13 -20
  90. package/dist/oauth/use-oauth-callback.js.map +1 -1
  91. package/dist/push/use-push-subscription.cjs +1 -1
  92. package/dist/push/use-push-subscription.cjs.map +1 -1
  93. package/dist/push/use-push-subscription.js +42 -42
  94. package/dist/push/use-push-subscription.js.map +1 -1
  95. package/dist/push/web-push-client.cjs +1 -1
  96. package/dist/push/web-push-client.cjs.map +1 -1
  97. package/dist/push/web-push-client.js +23 -3
  98. package/dist/push/web-push-client.js.map +1 -1
  99. package/dist/query/offline-persistence.cjs +1 -1
  100. package/dist/query/offline-persistence.cjs.map +1 -1
  101. package/dist/query/offline-persistence.js +68 -23
  102. package/dist/query/offline-persistence.js.map +1 -1
  103. package/dist/styles/component/Toast.css +6 -1
  104. package/dist/styles/feedback.css +6 -1
  105. package/dist/styles.css +1 -1
  106. package/dist/sw/background-sync.cjs +1 -1
  107. package/dist/sw/background-sync.cjs.map +1 -1
  108. package/dist/sw/background-sync.js +272 -60
  109. package/dist/sw/background-sync.js.map +1 -1
  110. package/dist/sw/cache-inspect.cjs.map +1 -1
  111. package/dist/sw/cache-inspect.js.map +1 -1
  112. package/dist/sw/cache.cjs +1 -1
  113. package/dist/sw/cache.cjs.map +1 -1
  114. package/dist/sw/cache.js +91 -64
  115. package/dist/sw/cache.js.map +1 -1
  116. package/dist/sw/credential-headers.cjs +2 -0
  117. package/dist/sw/credential-headers.cjs.map +1 -0
  118. package/dist/sw/credential-headers.js +17 -0
  119. package/dist/sw/credential-headers.js.map +1 -0
  120. package/dist/sw/session-data.cjs +2 -0
  121. package/dist/sw/session-data.cjs.map +1 -0
  122. package/dist/sw/session-data.js +17 -0
  123. package/dist/sw/session-data.js.map +1 -0
  124. package/dist/sw.cjs +1 -1
  125. package/dist/sw.d.ts +309 -23
  126. package/dist/sw.js +7 -5
  127. package/dist/telemetry/app-error-queue.cjs +1 -1
  128. package/dist/telemetry/app-error-queue.cjs.map +1 -1
  129. package/dist/telemetry/app-error-queue.js +19 -14
  130. package/dist/telemetry/app-error-queue.js.map +1 -1
  131. package/dist/telemetry/app-error-reporter.cjs +1 -1
  132. package/dist/telemetry/app-error-reporter.cjs.map +1 -1
  133. package/dist/telemetry/app-error-reporter.js +75 -34
  134. package/dist/telemetry/app-error-reporter.js.map +1 -1
  135. package/dist/telemetry/app-error-transport.cjs +1 -1
  136. package/dist/telemetry/app-error-transport.cjs.map +1 -1
  137. package/dist/telemetry/app-error-transport.js +16 -3
  138. package/dist/telemetry/app-error-transport.js.map +1 -1
  139. package/dist/tempest-react-sdk.cjs +1 -1
  140. package/dist/tempest-react-sdk.d.ts +1183 -64
  141. package/dist/tempest-react-sdk.js +160 -156
  142. package/dist/utils/csv.cjs +3 -3
  143. package/dist/utils/csv.cjs.map +1 -1
  144. package/dist/utils/csv.js +18 -14
  145. package/dist/utils/csv.js.map +1 -1
  146. package/dist/utils/owner-fingerprint.cjs +2 -0
  147. package/dist/utils/owner-fingerprint.cjs.map +1 -0
  148. package/dist/utils/owner-fingerprint.js +18 -0
  149. package/dist/utils/owner-fingerprint.js.map +1 -0
  150. package/dist/utils/safe-url.cjs +2 -0
  151. package/dist/utils/safe-url.cjs.map +1 -0
  152. package/dist/utils/safe-url.js +39 -0
  153. package/dist/utils/safe-url.js.map +1 -0
  154. package/dist/vite/tempest-csp.cjs +2 -2
  155. package/dist/vite/tempest-csp.cjs.map +1 -1
  156. package/dist/vite/tempest-csp.js +130 -64
  157. package/dist/vite/tempest-csp.js.map +1 -1
  158. package/dist/vite.d.ts +23 -5
  159. package/dist/webrtc/peer-link.cjs +1 -1
  160. package/dist/webrtc/peer-link.cjs.map +1 -1
  161. package/dist/webrtc/peer-link.js +2 -10
  162. package/dist/webrtc/peer-link.js.map +1 -1
  163. package/dist/webrtc/peer-mesh.cjs +1 -1
  164. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  165. package/dist/webrtc/peer-mesh.js +110 -72
  166. package/dist/webrtc/peer-mesh.js.map +1 -1
  167. package/package.json +1 -1
  168. package/template-pwa/src/pwa.tsx +22 -0
  169. package/template-pwa/src/sw.ts +27 -14
  170. package/dist/components/Markdown/markdown-url.cjs +0 -2
  171. package/dist/components/Markdown/markdown-url.cjs.map +0 -1
  172. package/dist/components/Markdown/markdown-url.js +0 -28
  173. package/dist/components/Markdown/markdown-url.js.map +0 -1
@@ -16,22 +16,21 @@ function r(e, t) {
16
16
  });
17
17
  }
18
18
  async function i(e, t = {}) {
19
- let { retries: i = 3, initialDelay: a = 300, maxDelay: o = 1e4, respectRetryAfter: s = !0, shouldRetry: c = n, onRetry: l, signal: u } = t, d = 0, f;
20
- for (; d < i;) {
19
+ let { retries: i = 3, initialDelay: a = 300, maxDelay: o = 1e4, respectRetryAfter: s = !0, shouldRetry: c = n, onRetry: l, signal: u } = t, d = Number.isNaN(i) ? 3 : Math.max(1, Math.floor(i)), f = 0;
20
+ for (;;) {
21
21
  if (u?.aborted) throw new DOMException("Aborted", "AbortError");
22
22
  try {
23
23
  return await e();
24
24
  } catch (e) {
25
- if (f = e, d += 1, d >= i || !c(e, d)) throw e;
26
- let t = s && typeof e?.retryAfter == "number" ? e.retryAfter * 1e3 : null, n = Math.min(t === null ? a * 2 ** (d - 1) : t, o);
25
+ if (f += 1, f >= d || !c(e, f)) throw e;
26
+ let t = s && typeof e?.retryAfter == "number" ? e.retryAfter * 1e3 : null, n = Math.min(t === null ? a * 2 ** (f - 1) : t, o);
27
27
  l?.({
28
- attempt: d,
28
+ attempt: f,
29
29
  delay: n,
30
30
  error: e
31
31
  }), await r(n, u);
32
32
  }
33
33
  }
34
- throw f;
35
34
  }
36
35
  //#endregion
37
36
  export { i as retry };
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","names":[],"sources":["../../src/http/retry.ts"],"sourcesContent":["import { isApiError, isRetriableStatus } from \"./errors\";\n\nexport interface RetryOptions {\n /** Max attempts (including the first). Default: 3. */\n retries?: number;\n /** Initial backoff in ms. Doubles each attempt, capped at `maxDelay`. Default: 300. */\n initialDelay?: number;\n /** Maximum delay between attempts. Default: 10_000. */\n maxDelay?: number;\n /**\n * Honor a `Retry-After` hint on the thrown error (`error.retryAfter`, in\n * seconds — populated by {@link createApiClient} on `429`/`503`). When\n * present it overrides the exponential backoff for that attempt. The value\n * is capped at `maxDelay`. Default: true.\n */\n respectRetryAfter?: boolean;\n /**\n * Return false to stop retrying for a specific error.\n *\n * Default: replay anything that is not a recognisable API error — a\n * transport failure has no status to judge — and, for one that is, only the\n * statuses {@link isRetriableStatus} accepts. A `403` on an admin-only\n * endpoint and a `404` for a deleted record are the server's final answer;\n * repeating them spends the caller's time to show the same error twice.\n */\n shouldRetry?: (error: unknown, attempt: number) => boolean;\n /** Called before each retry with the upcoming delay. */\n onRetry?: (info: { attempt: number; delay: number; error: unknown }) => void;\n /** Cancel pending retries. */\n signal?: AbortSignal;\n}\n\n/**\n * The default {@link RetryOptions.shouldRetry}.\n *\n * Permissive about what it cannot classify and strict about what it can: an\n * error with no API shape may well be a transport failure, while an API error\n * carrying a deliberate refusal will answer the same on every attempt.\n *\n * @param error - Whatever the attempt threw.\n * @returns Whether the helper should try again.\n */\nfunction defaultShouldRetry(error: unknown): boolean {\n return !isApiError(error) || isRetriableStatus(error.status);\n}\n\nfunction wait(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n}\n\n/**\n * Run `factory()` with exponential backoff. Each attempt awaits an\n * increasing delay capped at `maxDelay`. Throws the last error if every\n * attempt fails.\n *\n * @example\n * const data = await retry(() => api.get(\"/flaky\"), { retries: 5 });\n */\nexport async function retry<T>(factory: () => Promise<T>, options: RetryOptions = {}): Promise<T> {\n const {\n retries = 3,\n initialDelay = 300,\n maxDelay = 10_000,\n respectRetryAfter = true,\n shouldRetry = defaultShouldRetry,\n onRetry,\n signal,\n } = options;\n\n let attempt = 0;\n let lastError: unknown;\n\n while (attempt < retries) {\n if (signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\");\n try {\n return await factory();\n } catch (error) {\n lastError = error;\n attempt += 1;\n if (attempt >= retries || !shouldRetry(error, attempt)) {\n throw error;\n }\n const retryAfter =\n respectRetryAfter &&\n typeof (error as { retryAfter?: unknown })?.retryAfter === \"number\"\n ? (error as { retryAfter: number }).retryAfter * 1000\n : null;\n const delay =\n retryAfter !== null\n ? Math.min(retryAfter, maxDelay)\n : Math.min(initialDelay * 2 ** (attempt - 1), maxDelay);\n onRetry?.({ attempt, delay, error });\n await wait(delay, signal);\n }\n }\n\n throw lastError;\n}\n"],"mappings":";;AA0CA,SAAS,EAAmB,GAAyB;CACjD,OAAO,CAAC,EAAW,CAAK,KAAK,EAAkB,EAAM,MAAM;AAC/D;AAEA,SAAS,EAAK,GAAY,GAAqC;CAC3D,OAAO,IAAI,SAAe,GAAS,MAAW;EAC1C,IAAI,GAAQ,SAAS;GACjB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;GAChD;EACJ;EACA,IAAM,IAAQ,WAAW,GAAS,CAAE;EACpC,GAAQ,iBACJ,eACM;GAEF,AADA,aAAa,CAAK,GAClB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;EACpD,GACA,EAAE,MAAM,GAAK,CACjB;CACJ,CAAC;AACL;AAUA,eAAsB,EAAS,GAA2B,IAAwB,CAAC,GAAe;CAC9F,IAAM,EACF,aAAU,GACV,kBAAe,KACf,cAAW,KACX,uBAAoB,IACpB,iBAAc,GACd,YACA,cACA,GAEA,IAAU,GACV;CAEJ,OAAO,IAAU,IAAS;EACtB,IAAI,GAAQ,SAAS,MAAM,IAAI,aAAa,WAAW,YAAY;EACnE,IAAI;GACA,OAAO,MAAM,EAAQ;EACzB,SAAS,GAAO;GAGZ,IAFA,IAAY,GACZ,KAAW,GACP,KAAW,KAAW,CAAC,EAAY,GAAO,CAAO,GACjD,MAAM;GAEV,IAAM,IACF,KACA,OAAQ,GAAoC,cAAe,WACpD,EAAiC,aAAa,MAC/C,MACJ,IAGI,KAAK,IAFX,MAAe,OAEA,IAAe,MAAM,IAAU,KAD/B,GACmC,CAAQ;GAE9D,AADA,IAAU;IAAE;IAAS;IAAO;GAAM,CAAC,GACnC,MAAM,EAAK,GAAO,CAAM;EAC5B;CACJ;CAEA,MAAM;AACV"}
1
+ {"version":3,"file":"retry.js","names":[],"sources":["../../src/http/retry.ts"],"sourcesContent":["import { isApiError, isRetriableStatus } from \"./errors\";\n\nexport interface RetryOptions {\n /**\n * Total attempts, the first one included. Default: 3 — one call plus two\n * replays.\n *\n * The first attempt always runs: `0`, a negative number or a fraction below\n * one all mean \"a single attempt, no replay\", and that attempt's error is the\n * one the promise rejects with. A fraction is rounded down; `NaN` falls back\n * to the default.\n *\n * The name reads like \"replays after the first\", but every surface that\n * forwards it has always counted totals — {@link createApiClient}'s `retry`,\n * `createResumableUpload` (\"5 attempts\"), and `lazyWithRetry`'s own\n * `retries`. Switching to \"replays\" would add one request to every caller\n * that passes a number, which no change of default can hide, so the count\n * stays a total and only the values that ran nothing were fixed (#486).\n */\n retries?: number;\n /** Initial backoff in ms. Doubles each attempt, capped at `maxDelay`. Default: 300. */\n initialDelay?: number;\n /** Maximum delay between attempts. Default: 10_000. */\n maxDelay?: number;\n /**\n * Honor a `Retry-After` hint on the thrown error (`error.retryAfter`, in\n * seconds — populated by {@link createApiClient} on `429`/`503`). When\n * present it overrides the exponential backoff for that attempt. The value\n * is capped at `maxDelay`. Default: true.\n */\n respectRetryAfter?: boolean;\n /**\n * Return false to stop retrying for a specific error.\n *\n * Default: replay anything that is not a recognisable API error — a\n * transport failure has no status to judge — and, for one that is, only the\n * statuses {@link isRetriableStatus} accepts. A `403` on an admin-only\n * endpoint and a `404` for a deleted record are the server's final answer;\n * repeating them spends the caller's time to show the same error twice.\n */\n shouldRetry?: (error: unknown, attempt: number) => boolean;\n /** Called before each retry with the upcoming delay. */\n onRetry?: (info: { attempt: number; delay: number; error: unknown }) => void;\n /** Cancel pending retries. */\n signal?: AbortSignal;\n}\n\n/**\n * The default {@link RetryOptions.shouldRetry}.\n *\n * Permissive about what it cannot classify and strict about what it can: an\n * error with no API shape may well be a transport failure, while an API error\n * carrying a deliberate refusal will answer the same on every attempt.\n *\n * @param error - Whatever the attempt threw.\n * @returns Whether the helper should try again.\n */\nfunction defaultShouldRetry(error: unknown): boolean {\n return !isApiError(error) || isRetriableStatus(error.status);\n}\n\nfunction wait(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n}\n\n/**\n * Run `factory()` with exponential backoff. Each attempt awaits an\n * increasing delay capped at `maxDelay`. Throws the last error if every\n * attempt fails.\n *\n * `retries` is the **total** number of attempts, never fewer than one — see\n * {@link RetryOptions.retries}. `retries: 5` means one call plus four replays.\n *\n * @param factory - Starts one attempt; called again for every replay.\n * @param options - Attempt count, backoff and the retry predicate.\n * @returns What the first successful attempt resolved with.\n * @throws The error of the last attempt, or an `AbortError` once `signal` aborts.\n *\n * @example\n * const data = await retry(() => api.get(\"/flaky\"), { retries: 5 });\n */\nexport async function retry<T>(factory: () => Promise<T>, options: RetryOptions = {}): Promise<T> {\n const {\n retries = 3,\n initialDelay = 300,\n maxDelay = 10_000,\n respectRetryAfter = true,\n shouldRetry = defaultShouldRetry,\n onRetry,\n signal,\n } = options;\n\n const attempts = Number.isNaN(retries) ? 3 : Math.max(1, Math.floor(retries));\n let attempt = 0;\n\n for (;;) {\n if (signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\");\n try {\n return await factory();\n } catch (error) {\n attempt += 1;\n if (attempt >= attempts || !shouldRetry(error, attempt)) {\n throw error;\n }\n const retryAfter =\n respectRetryAfter &&\n typeof (error as { retryAfter?: unknown })?.retryAfter === \"number\"\n ? (error as { retryAfter: number }).retryAfter * 1000\n : null;\n const delay =\n retryAfter !== null\n ? Math.min(retryAfter, maxDelay)\n : Math.min(initialDelay * 2 ** (attempt - 1), maxDelay);\n onRetry?.({ attempt, delay, error });\n await wait(delay, signal);\n }\n }\n}\n"],"mappings":";;AAyDA,SAAS,EAAmB,GAAyB;CACjD,OAAO,CAAC,EAAW,CAAK,KAAK,EAAkB,EAAM,MAAM;AAC/D;AAEA,SAAS,EAAK,GAAY,GAAqC;CAC3D,OAAO,IAAI,SAAe,GAAS,MAAW;EAC1C,IAAI,GAAQ,SAAS;GACjB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;GAChD;EACJ;EACA,IAAM,IAAQ,WAAW,GAAS,CAAE;EACpC,GAAQ,iBACJ,eACM;GAEF,AADA,aAAa,CAAK,GAClB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;EACpD,GACA,EAAE,MAAM,GAAK,CACjB;CACJ,CAAC;AACL;AAkBA,eAAsB,EAAS,GAA2B,IAAwB,CAAC,GAAe;CAC9F,IAAM,EACF,aAAU,GACV,kBAAe,KACf,cAAW,KACX,uBAAoB,IACpB,iBAAc,GACd,YACA,cACA,GAEE,IAAW,OAAO,MAAM,CAAO,IAAI,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,CAAO,CAAC,GACxE,IAAU;CAEd,SAAS;EACL,IAAI,GAAQ,SAAS,MAAM,IAAI,aAAa,WAAW,YAAY;EACnE,IAAI;GACA,OAAO,MAAM,EAAQ;EACzB,SAAS,GAAO;GAEZ,IADA,KAAW,GACP,KAAW,KAAY,CAAC,EAAY,GAAO,CAAO,GAClD,MAAM;GAEV,IAAM,IACF,KACA,OAAQ,GAAoC,cAAe,WACpD,EAAiC,aAAa,MAC/C,MACJ,IAGI,KAAK,IAFX,MAAe,OAEA,IAAe,MAAM,IAAU,KAD/B,GACmC,CAAQ;GAE9D,AADA,IAAU;IAAE;IAAS;IAAO;GAAM,CAAC,GACnC,MAAM,EAAK,GAAO,CAAM;EAC5B;CACJ;AACJ"}
@@ -0,0 +1,2 @@
1
+ var e=class extends Error{reason;providerError;constructor(e,t){super(e===`provider_error`?`OAuth provider returned an error: ${t??`unknown`}`:`OAuth callback is missing the "${e===`missing_state`?`state`:`code`}" param`),this.name=`OAuthCallbackError`,this.reason=e,this.providerError=t}},t=[`code`,`state`];function n(){let e=new URL(window.location.href);if(t.some(t=>e.searchParams.has(t))){for(let n of t)e.searchParams.delete(n);window.history.replaceState(window.history.state,``,`${e.pathname}${e.search}${e.hash}`)}}function r(t,r={}){let i=t===void 0,{scrubUrl:a=i}=r,o=t??globalThis.location?.search??``,s=typeof o==`string`?new URLSearchParams(o):o,c=s.get(`error`),l=s.get(`state`),u=s.get(`code`);if(a&&typeof window<`u`&&n(),c)throw new e(`provider_error`,c);if(!l)throw new e(`missing_state`);if(!u)throw new e(`missing_code`);return{code:u,state:l}}async function i(e,t,n={}){let{prefix:i=`/auth`,search:a,scrubUrl:o,signal:s}=n,{code:c,state:l}=r(a,{scrubUrl:o}),u=i.replace(/\/+$/,``);return e.get(`${u}/oauth/${encodeURIComponent(t)}/callback`,{params:{code:c,state:l},credentials:`include`,skipAuth:!0,retry:!1,signal:s})}exports.OAuthCallbackError=e,exports.exchangeOAuthCallback=i,exports.readOAuthCallbackParams=r;
2
+ //# sourceMappingURL=callback.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.cjs","names":[],"sources":["../../src/oauth/callback.ts"],"sourcesContent":["import type { ApiClient } from \"@/http/types\";\n\n/** Why {@link readOAuthCallbackParams} refused a callback URL. */\nexport type OAuthCallbackErrorReason = \"provider_error\" | \"missing_state\" | \"missing_code\";\n\n/**\n * A provider redirect that cannot be exchanged for a session.\n *\n * Thrown before any request leaves the browser, so a forged or truncated\n * callback never reaches the backend's exchange route.\n */\nexport class OAuthCallbackError extends Error {\n /** Which part of the callback was wrong. */\n readonly reason: OAuthCallbackErrorReason;\n /** The provider's `error` param (e.g. `\"access_denied\"`), when `reason` is `\"provider_error\"`. */\n readonly providerError?: string;\n\n /**\n * @param reason - Which part of the callback was wrong.\n * @param providerError - The provider's `error` param, when it sent one.\n */\n constructor(reason: OAuthCallbackErrorReason, providerError?: string) {\n super(\n reason === \"provider_error\"\n ? `OAuth provider returned an error: ${providerError ?? \"unknown\"}`\n : `OAuth callback is missing the \"${reason === \"missing_state\" ? \"state\" : \"code\"}\" param`,\n );\n this.name = \"OAuthCallbackError\";\n this.reason = reason;\n this.providerError = providerError;\n }\n}\n\n/** The two values a provider appends to the redirect URI on success. */\nexport interface OAuthCallbackParams {\n /** Single-use authorization code. */\n code: string;\n /**\n * The CSRF `state` the backend minted when the flow started. Forwarded\n * verbatim: only the backend holds the value it must match.\n */\n state: string;\n}\n\n/** Options for {@link readOAuthCallbackParams}. */\nexport interface ReadOAuthCallbackOptions {\n /**\n * Remove `code` and `state` from the address bar after reading them, with\n * `history.replaceState` — other params, the hash and `history.state` are\n * kept. Default: `true` when reading `window.location`, `false` when an\n * explicit `search` is passed (there is no URL of yours to clean then).\n */\n scrubUrl?: boolean;\n}\n\nconst CALLBACK_SECRETS: readonly string[] = [\"code\", \"state\"];\n\n/**\n * Drop `code` and `state` from the current URL without a navigation.\n *\n * Left in place, the single-use code sits in the history entry, in a bookmark,\n * in a shared screenshot and in the `Referer` of the next request. Passing\n * `history.state` back keeps a router's own bookkeeping (React Router's `idx`)\n * intact. No-op when neither param is present.\n */\nfunction scrubCallbackUrl(): void {\n const url = new URL(window.location.href);\n if (!CALLBACK_SECRETS.some((key) => url.searchParams.has(key))) return;\n for (const key of CALLBACK_SECRETS) url.searchParams.delete(key);\n window.history.replaceState(\n window.history.state,\n \"\",\n `${url.pathname}${url.search}${url.hash}`,\n );\n}\n\n/**\n * Read `code` and `state` from a provider's redirect, refusing an incomplete one.\n *\n * The `state` is what ties the callback to the browser that started the flow.\n * Without it, an attacker sends the victim to `/callback?code=<attacker's code>`\n * and the victim ends up signed in to the attacker's account — a login CSRF.\n * The old recipe read only `code` and posted `{ code }`, so the `state` never\n * reached a backend that could compare it (#449). This reader makes dropping it\n * impossible: it returns both or throws.\n *\n * It does **not** compare the state. In the `tempest-fastapi-sdk` flow the\n * backend mints it in `GET {prefix}/oauth/{provider}/login`, keeps it in an\n * `HttpOnly` cookie bound to the provider, and checks it with\n * `hmac.compare_digest` in the callback (single use, `AUTH_OAUTH_STATE_TTL_SECONDS`\n * expiry). The browser cannot read that cookie, so any check here would be\n * theatre; forwarding the value is the browser's whole job.\n *\n * When it reads `window.location`, it also removes `code` and `state` from the\n * address bar (see {@link ReadOAuthCallbackOptions.scrubUrl}) — before\n * validating, so a refused callback is cleaned too.\n *\n * @param search - The query string or params to read. Default: `window.location.search`.\n * @param options - Whether to scrub the URL.\n * @returns The `code` and `state`.\n * @throws {OAuthCallbackError} `\"provider_error\"` when the provider sent `error`\n * (the user declined, for instance), `\"missing_state\"` without `state`,\n * `\"missing_code\"` without `code`.\n *\n * @example\n * const { code, state } = readOAuthCallbackParams(\"?code=4%2F0A&state=xyz\");\n */\nexport function readOAuthCallbackParams(\n search?: string | URLSearchParams,\n options: ReadOAuthCallbackOptions = {},\n): OAuthCallbackParams {\n const fromLocation = search === undefined;\n const { scrubUrl = fromLocation } = options;\n const source = search ?? globalThis.location?.search ?? \"\";\n const params = typeof source === \"string\" ? new URLSearchParams(source) : source;\n const error = params.get(\"error\");\n const state = params.get(\"state\");\n const code = params.get(\"code\");\n if (scrubUrl && typeof window !== \"undefined\") scrubCallbackUrl();\n if (error) throw new OAuthCallbackError(\"provider_error\", error);\n if (!state) throw new OAuthCallbackError(\"missing_state\");\n if (!code) throw new OAuthCallbackError(\"missing_code\");\n return { code, state };\n}\n\n/** Options for {@link exchangeOAuthCallback}. */\nexport interface ExchangeOAuthCallbackOptions {\n /**\n * Path the backend's auth router is mounted at — the `prefix` given to\n * `make_auth_router`. Default: `\"/auth\"`.\n */\n prefix?: string;\n /** Where to read the provider's redirect from. Default: `window.location.search`. */\n search?: string | URLSearchParams;\n /** Forwarded to {@link readOAuthCallbackParams}. Default: scrub when reading `window.location`. */\n scrubUrl?: boolean;\n /** Abort the exchange. */\n signal?: AbortSignal;\n}\n\n/**\n * Finish a `tempest-fastapi-sdk` social login from the SPA's callback route.\n *\n * Reads the redirect with {@link readOAuthCallbackParams} and forwards `code`\n * **and** `state` to `GET {prefix}/oauth/{provider}/callback`, where the backend\n * compares the state against its `HttpOnly` cookie, redeems the code and\n * answers with the same JWT pair `POST {prefix}/login` returns.\n *\n * The request goes with `credentials: \"include\"` because the state cookie is\n * the other half of the comparison; with `skipAuth`, since there is no session\n * yet and a `401` here means a forged or expired callback, not an expired token\n * to refresh; and with `retry: false`, because the backend clears the state\n * cookie on the first attempt and a code redeems once.\n *\n * Pass it as the `exchange` of {@link useOAuthCallback}, which runs it once even\n * under StrictMode.\n *\n * @param api - The client from `createApiClient` (or `createTempestAuth().api`).\n * @param provider - The provider key registered on the backend (e.g. `\"google\"`).\n * @param options - Router prefix, the query string to read, abort signal.\n * @returns The backend's login response.\n * @throws {OAuthCallbackError} Before any request, when the redirect carries\n * `error` or lacks `state` or `code`.\n *\n * @example\n * const { loading, error } = useOAuthCallback<LoginResponse>({\n * exchange: () => exchangeOAuthCallback<LoginResponse>(api, \"google\"),\n * onSuccess: () => navigate(\"/\", { replace: true }),\n * });\n */\nexport async function exchangeOAuthCallback<T>(\n api: ApiClient,\n provider: string,\n options: ExchangeOAuthCallbackOptions = {},\n): Promise<T> {\n const { prefix = \"/auth\", search, scrubUrl, signal } = options;\n const { code, state } = readOAuthCallbackParams(search, { scrubUrl });\n const base = prefix.replace(/\\/+$/, \"\");\n return api.get<T>(`${base}/oauth/${encodeURIComponent(provider)}/callback`, {\n params: { code, state },\n credentials: \"include\",\n skipAuth: true,\n retry: false,\n signal,\n });\n}\n"],"mappings":"AAWA,IAAa,EAAb,cAAwC,KAAM,CAE1C,OAEA,cAMA,YAAY,EAAkC,EAAwB,CAClE,MACI,IAAW,iBACL,qCAAqC,GAAiB,YACtD,kCAAkC,IAAW,gBAAkB,QAAU,OAAO,QAC1F,EACA,KAAK,KAAO,qBACZ,KAAK,OAAS,EACd,KAAK,cAAgB,CACzB,CACJ,EAwBM,EAAsC,CAAC,OAAQ,OAAO,EAU5D,SAAS,GAAyB,CAC9B,IAAM,EAAM,IAAI,IAAI,OAAO,SAAS,IAAI,EACnC,KAAiB,KAAM,GAAQ,EAAI,aAAa,IAAI,CAAG,CAAC,EAC7D,KAAK,IAAM,KAAO,EAAkB,EAAI,aAAa,OAAO,CAAG,EAC/D,OAAO,QAAQ,aACX,OAAO,QAAQ,MACf,GACA,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,MACvC,CAL+D,CAMnE,CAiCA,SAAgB,EACZ,EACA,EAAoC,CAAC,EAClB,CACnB,IAAM,EAAe,IAAW,IAAA,GAC1B,CAAE,WAAW,GAAiB,EAC9B,EAAS,GAAU,WAAW,UAAU,QAAU,GAClD,EAAS,OAAO,GAAW,SAAW,IAAI,gBAAgB,CAAM,EAAI,EACpE,EAAQ,EAAO,IAAI,OAAO,EAC1B,EAAQ,EAAO,IAAI,OAAO,EAC1B,EAAO,EAAO,IAAI,MAAM,EAE9B,GADI,GAAY,OAAO,OAAW,KAAa,EAAiB,EAC5D,EAAO,MAAM,IAAI,EAAmB,iBAAkB,CAAK,EAC/D,GAAI,CAAC,EAAO,MAAM,IAAI,EAAmB,eAAe,EACxD,GAAI,CAAC,EAAM,MAAM,IAAI,EAAmB,cAAc,EACtD,MAAO,CAAE,OAAM,OAAM,CACzB,CA+CA,eAAsB,EAClB,EACA,EACA,EAAwC,CAAC,EAC/B,CACV,GAAM,CAAE,SAAS,QAAS,SAAQ,WAAU,UAAW,EACjD,CAAE,OAAM,SAAU,EAAwB,EAAQ,CAAE,UAAS,CAAC,EAC9D,EAAO,EAAO,QAAQ,OAAQ,EAAE,EACtC,OAAO,EAAI,IAAO,GAAG,EAAK,SAAS,mBAAmB,CAAQ,EAAE,WAAY,CACxE,OAAQ,CAAE,OAAM,OAAM,EACtB,YAAa,UACb,SAAU,GACV,MAAO,GACP,QACJ,CAAC,CACL"}
@@ -0,0 +1,42 @@
1
+ //#region src/oauth/callback.ts
2
+ var e = class extends Error {
3
+ reason;
4
+ providerError;
5
+ constructor(e, t) {
6
+ super(e === "provider_error" ? `OAuth provider returned an error: ${t ?? "unknown"}` : `OAuth callback is missing the "${e === "missing_state" ? "state" : "code"}" param`), this.name = "OAuthCallbackError", this.reason = e, this.providerError = t;
7
+ }
8
+ }, t = ["code", "state"];
9
+ function n() {
10
+ let e = new URL(window.location.href);
11
+ if (t.some((t) => e.searchParams.has(t))) {
12
+ for (let n of t) e.searchParams.delete(n);
13
+ window.history.replaceState(window.history.state, "", `${e.pathname}${e.search}${e.hash}`);
14
+ }
15
+ }
16
+ function r(t, r = {}) {
17
+ let i = t === void 0, { scrubUrl: a = i } = r, o = t ?? globalThis.location?.search ?? "", s = typeof o == "string" ? new URLSearchParams(o) : o, c = s.get("error"), l = s.get("state"), u = s.get("code");
18
+ if (a && typeof window < "u" && n(), c) throw new e("provider_error", c);
19
+ if (!l) throw new e("missing_state");
20
+ if (!u) throw new e("missing_code");
21
+ return {
22
+ code: u,
23
+ state: l
24
+ };
25
+ }
26
+ async function i(e, t, n = {}) {
27
+ let { prefix: i = "/auth", search: a, scrubUrl: o, signal: s } = n, { code: c, state: l } = r(a, { scrubUrl: o }), u = i.replace(/\/+$/, "");
28
+ return e.get(`${u}/oauth/${encodeURIComponent(t)}/callback`, {
29
+ params: {
30
+ code: c,
31
+ state: l
32
+ },
33
+ credentials: "include",
34
+ skipAuth: !0,
35
+ retry: !1,
36
+ signal: s
37
+ });
38
+ }
39
+ //#endregion
40
+ export { e as OAuthCallbackError, i as exchangeOAuthCallback, r as readOAuthCallbackParams };
41
+
42
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","names":[],"sources":["../../src/oauth/callback.ts"],"sourcesContent":["import type { ApiClient } from \"@/http/types\";\n\n/** Why {@link readOAuthCallbackParams} refused a callback URL. */\nexport type OAuthCallbackErrorReason = \"provider_error\" | \"missing_state\" | \"missing_code\";\n\n/**\n * A provider redirect that cannot be exchanged for a session.\n *\n * Thrown before any request leaves the browser, so a forged or truncated\n * callback never reaches the backend's exchange route.\n */\nexport class OAuthCallbackError extends Error {\n /** Which part of the callback was wrong. */\n readonly reason: OAuthCallbackErrorReason;\n /** The provider's `error` param (e.g. `\"access_denied\"`), when `reason` is `\"provider_error\"`. */\n readonly providerError?: string;\n\n /**\n * @param reason - Which part of the callback was wrong.\n * @param providerError - The provider's `error` param, when it sent one.\n */\n constructor(reason: OAuthCallbackErrorReason, providerError?: string) {\n super(\n reason === \"provider_error\"\n ? `OAuth provider returned an error: ${providerError ?? \"unknown\"}`\n : `OAuth callback is missing the \"${reason === \"missing_state\" ? \"state\" : \"code\"}\" param`,\n );\n this.name = \"OAuthCallbackError\";\n this.reason = reason;\n this.providerError = providerError;\n }\n}\n\n/** The two values a provider appends to the redirect URI on success. */\nexport interface OAuthCallbackParams {\n /** Single-use authorization code. */\n code: string;\n /**\n * The CSRF `state` the backend minted when the flow started. Forwarded\n * verbatim: only the backend holds the value it must match.\n */\n state: string;\n}\n\n/** Options for {@link readOAuthCallbackParams}. */\nexport interface ReadOAuthCallbackOptions {\n /**\n * Remove `code` and `state` from the address bar after reading them, with\n * `history.replaceState` — other params, the hash and `history.state` are\n * kept. Default: `true` when reading `window.location`, `false` when an\n * explicit `search` is passed (there is no URL of yours to clean then).\n */\n scrubUrl?: boolean;\n}\n\nconst CALLBACK_SECRETS: readonly string[] = [\"code\", \"state\"];\n\n/**\n * Drop `code` and `state` from the current URL without a navigation.\n *\n * Left in place, the single-use code sits in the history entry, in a bookmark,\n * in a shared screenshot and in the `Referer` of the next request. Passing\n * `history.state` back keeps a router's own bookkeeping (React Router's `idx`)\n * intact. No-op when neither param is present.\n */\nfunction scrubCallbackUrl(): void {\n const url = new URL(window.location.href);\n if (!CALLBACK_SECRETS.some((key) => url.searchParams.has(key))) return;\n for (const key of CALLBACK_SECRETS) url.searchParams.delete(key);\n window.history.replaceState(\n window.history.state,\n \"\",\n `${url.pathname}${url.search}${url.hash}`,\n );\n}\n\n/**\n * Read `code` and `state` from a provider's redirect, refusing an incomplete one.\n *\n * The `state` is what ties the callback to the browser that started the flow.\n * Without it, an attacker sends the victim to `/callback?code=<attacker's code>`\n * and the victim ends up signed in to the attacker's account — a login CSRF.\n * The old recipe read only `code` and posted `{ code }`, so the `state` never\n * reached a backend that could compare it (#449). This reader makes dropping it\n * impossible: it returns both or throws.\n *\n * It does **not** compare the state. In the `tempest-fastapi-sdk` flow the\n * backend mints it in `GET {prefix}/oauth/{provider}/login`, keeps it in an\n * `HttpOnly` cookie bound to the provider, and checks it with\n * `hmac.compare_digest` in the callback (single use, `AUTH_OAUTH_STATE_TTL_SECONDS`\n * expiry). The browser cannot read that cookie, so any check here would be\n * theatre; forwarding the value is the browser's whole job.\n *\n * When it reads `window.location`, it also removes `code` and `state` from the\n * address bar (see {@link ReadOAuthCallbackOptions.scrubUrl}) — before\n * validating, so a refused callback is cleaned too.\n *\n * @param search - The query string or params to read. Default: `window.location.search`.\n * @param options - Whether to scrub the URL.\n * @returns The `code` and `state`.\n * @throws {OAuthCallbackError} `\"provider_error\"` when the provider sent `error`\n * (the user declined, for instance), `\"missing_state\"` without `state`,\n * `\"missing_code\"` without `code`.\n *\n * @example\n * const { code, state } = readOAuthCallbackParams(\"?code=4%2F0A&state=xyz\");\n */\nexport function readOAuthCallbackParams(\n search?: string | URLSearchParams,\n options: ReadOAuthCallbackOptions = {},\n): OAuthCallbackParams {\n const fromLocation = search === undefined;\n const { scrubUrl = fromLocation } = options;\n const source = search ?? globalThis.location?.search ?? \"\";\n const params = typeof source === \"string\" ? new URLSearchParams(source) : source;\n const error = params.get(\"error\");\n const state = params.get(\"state\");\n const code = params.get(\"code\");\n if (scrubUrl && typeof window !== \"undefined\") scrubCallbackUrl();\n if (error) throw new OAuthCallbackError(\"provider_error\", error);\n if (!state) throw new OAuthCallbackError(\"missing_state\");\n if (!code) throw new OAuthCallbackError(\"missing_code\");\n return { code, state };\n}\n\n/** Options for {@link exchangeOAuthCallback}. */\nexport interface ExchangeOAuthCallbackOptions {\n /**\n * Path the backend's auth router is mounted at — the `prefix` given to\n * `make_auth_router`. Default: `\"/auth\"`.\n */\n prefix?: string;\n /** Where to read the provider's redirect from. Default: `window.location.search`. */\n search?: string | URLSearchParams;\n /** Forwarded to {@link readOAuthCallbackParams}. Default: scrub when reading `window.location`. */\n scrubUrl?: boolean;\n /** Abort the exchange. */\n signal?: AbortSignal;\n}\n\n/**\n * Finish a `tempest-fastapi-sdk` social login from the SPA's callback route.\n *\n * Reads the redirect with {@link readOAuthCallbackParams} and forwards `code`\n * **and** `state` to `GET {prefix}/oauth/{provider}/callback`, where the backend\n * compares the state against its `HttpOnly` cookie, redeems the code and\n * answers with the same JWT pair `POST {prefix}/login` returns.\n *\n * The request goes with `credentials: \"include\"` because the state cookie is\n * the other half of the comparison; with `skipAuth`, since there is no session\n * yet and a `401` here means a forged or expired callback, not an expired token\n * to refresh; and with `retry: false`, because the backend clears the state\n * cookie on the first attempt and a code redeems once.\n *\n * Pass it as the `exchange` of {@link useOAuthCallback}, which runs it once even\n * under StrictMode.\n *\n * @param api - The client from `createApiClient` (or `createTempestAuth().api`).\n * @param provider - The provider key registered on the backend (e.g. `\"google\"`).\n * @param options - Router prefix, the query string to read, abort signal.\n * @returns The backend's login response.\n * @throws {OAuthCallbackError} Before any request, when the redirect carries\n * `error` or lacks `state` or `code`.\n *\n * @example\n * const { loading, error } = useOAuthCallback<LoginResponse>({\n * exchange: () => exchangeOAuthCallback<LoginResponse>(api, \"google\"),\n * onSuccess: () => navigate(\"/\", { replace: true }),\n * });\n */\nexport async function exchangeOAuthCallback<T>(\n api: ApiClient,\n provider: string,\n options: ExchangeOAuthCallbackOptions = {},\n): Promise<T> {\n const { prefix = \"/auth\", search, scrubUrl, signal } = options;\n const { code, state } = readOAuthCallbackParams(search, { scrubUrl });\n const base = prefix.replace(/\\/+$/, \"\");\n return api.get<T>(`${base}/oauth/${encodeURIComponent(provider)}/callback`, {\n params: { code, state },\n credentials: \"include\",\n skipAuth: true,\n retry: false,\n signal,\n });\n}\n"],"mappings":";AAWA,IAAa,IAAb,cAAwC,MAAM;CAE1C;CAEA;CAMA,YAAY,GAAkC,GAAwB;EAQlE,AAPA,MACI,MAAW,mBACL,qCAAqC,KAAiB,cACtD,kCAAkC,MAAW,kBAAkB,UAAU,OAAO,QAC1F,GACA,KAAK,OAAO,sBACZ,KAAK,SAAS,GACd,KAAK,gBAAgB;CACzB;AACJ,GAwBM,IAAsC,CAAC,QAAQ,OAAO;AAU5D,SAAS,IAAyB;CAC9B,IAAM,IAAM,IAAI,IAAI,OAAO,SAAS,IAAI;CACnC,MAAiB,MAAM,MAAQ,EAAI,aAAa,IAAI,CAAG,CAAC,GAC7D;OAAK,IAAM,KAAO,GAAkB,EAAI,aAAa,OAAO,CAAG;EAC/D,OAAO,QAAQ,aACX,OAAO,QAAQ,OACf,IACA,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,MACvC;CAL+D;AAMnE;AAiCA,SAAgB,EACZ,GACA,IAAoC,CAAC,GAClB;CACnB,IAAM,IAAe,MAAW,KAAA,GAC1B,EAAE,cAAW,MAAiB,GAC9B,IAAS,KAAU,WAAW,UAAU,UAAU,IAClD,IAAS,OAAO,KAAW,WAAW,IAAI,gBAAgB,CAAM,IAAI,GACpE,IAAQ,EAAO,IAAI,OAAO,GAC1B,IAAQ,EAAO,IAAI,OAAO,GAC1B,IAAO,EAAO,IAAI,MAAM;CAE9B,IADI,KAAY,OAAO,SAAW,OAAa,EAAiB,GAC5D,GAAO,MAAM,IAAI,EAAmB,kBAAkB,CAAK;CAC/D,IAAI,CAAC,GAAO,MAAM,IAAI,EAAmB,eAAe;CACxD,IAAI,CAAC,GAAM,MAAM,IAAI,EAAmB,cAAc;CACtD,OAAO;EAAE;EAAM;CAAM;AACzB;AA+CA,eAAsB,EAClB,GACA,GACA,IAAwC,CAAC,GAC/B;CACV,IAAM,EAAE,YAAS,SAAS,WAAQ,aAAU,cAAW,GACjD,EAAE,SAAM,aAAU,EAAwB,GAAQ,EAAE,YAAS,CAAC,GAC9D,IAAO,EAAO,QAAQ,QAAQ,EAAE;CACtC,OAAO,EAAI,IAAO,GAAG,EAAK,SAAS,mBAAmB,CAAQ,EAAE,YAAY;EACxE,QAAQ;GAAE;GAAM;EAAM;EACtB,aAAa;EACb,UAAU;EACV,OAAO;EACP;CACJ,CAAC;AACL"}
@@ -1,2 +1,2 @@
1
- let e=require("react");function t(t){let{exchange:n,onSuccess:r,onError:i}=t,[a,o]=(0,e.useState)(null),[s,c]=(0,e.useState)(null),[l,u]=(0,e.useState)(`pending`),d=(0,e.useRef)(!1);return(0,e.useEffect)(()=>{if(d.current)return;d.current=!0;let e=!1;return n().then(t=>{if(!e)return o(t),u(`success`),r?.(t)}).catch(t=>{e||(c(t),u(`error`),i?.(t))}),()=>{e=!0}},[n,r,i]),{loading:l===`pending`,data:a,error:s,status:l}}exports.useOAuthCallback=t;
1
+ let e=require("react");function t(t){let[n,r]=(0,e.useState)(null),[i,a]=(0,e.useState)(null),[o,s]=(0,e.useState)(`pending`),c=(0,e.useRef)(t),l=(0,e.useRef)(!1),u=(0,e.useRef)(!1);return(0,e.useEffect)(()=>{c.current=t}),(0,e.useEffect)(()=>(u.current=!0,l.current||(l.current=!0,c.current.exchange().then(e=>{if(u.current)return r(e),s(`success`),c.current.onSuccess?.(e)}).catch(e=>{u.current&&(a(e),s(`error`),c.current.onError?.(e))})),()=>{u.current=!1}),[]),{loading:o===`pending`,data:n,error:i,status:o}}exports.useOAuthCallback=t;
2
2
  //# sourceMappingURL=use-oauth-callback.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-oauth-callback.cjs","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n * Survives React StrictMode double-mounting in dev — uses a ref guard to\n * ensure `exchange` runs once.\n *\n * @example\n * function OAuthCallback() {\n * const { loading, error } = useOAuthCallback({\n * exchange: async () => {\n * const code = new URLSearchParams(location.search).get(\"code\")!;\n * return api.post(\"/auth/google/exchange\", { body: { code } });\n * },\n * onSuccess: ({ token, user }) => {\n * useAuthStore.getState().setSession({ user, token });\n * navigate(\"/dashboard\", { replace: true });\n * },\n * onError: () => navigate(\"/login?error=oauth\", { replace: true }),\n * });\n *\n * if (loading) return <Spinner />;\n * if (error) return <ErrorState description=\"OAuth falhou\" />;\n * return null;\n * }\n */\nexport function useOAuthCallback<T>(\n options: UseOAuthCallbackOptions<T>,\n): UseOAuthCallbackResult<T> {\n const { exchange, onSuccess, onError } = options;\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [status, setStatus] = useState<\"pending\" | \"success\" | \"error\">(\"pending\");\n const ranRef = useRef<boolean>(false);\n\n useEffect(() => {\n if (ranRef.current) return;\n ranRef.current = true;\n let cancelled = false;\n exchange()\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setStatus(\"success\");\n return onSuccess?.(result);\n })\n .catch((err) => {\n if (cancelled) return;\n setError(err);\n setStatus(\"error\");\n onError?.(err);\n });\n return () => {\n cancelled = true;\n };\n }, [exchange, onSuccess, onError]);\n\n return { loading: status === \"pending\", data, error, status };\n}\n"],"mappings":"uBAiDA,SAAgB,EACZ,EACyB,CACzB,GAAM,CAAE,WAAU,YAAW,WAAY,EACnC,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAmB,IAAI,EACzC,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAkB,IAAI,EAC1C,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA0C,SAAS,EACzE,GAAA,EAAS,EAAA,OAAA,CAAgB,EAAK,EAwBpC,OAtBA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,EAAO,QAAS,OACpB,EAAO,QAAU,GACjB,IAAI,EAAY,GAchB,OAbA,EAAS,CAAC,CACL,KAAM,GAAW,CACV,MAGJ,OAFA,EAAQ,CAAM,EACd,EAAU,SAAS,EACZ,IAAY,CAAM,CAC7B,CAAC,CAAC,CACD,MAAO,GAAQ,CACR,IACJ,EAAS,CAAG,EACZ,EAAU,OAAO,EACjB,IAAU,CAAG,EACjB,CAAC,MACQ,CACT,EAAY,EAChB,CACJ,EAAG,CAAC,EAAU,EAAW,CAAO,CAAC,EAE1B,CAAE,QAAS,IAAW,UAAW,OAAM,QAAO,QAAO,CAChE"}
1
+ {"version":3,"file":"use-oauth-callback.cjs","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n * Runs `exchange` once per logical mount and keeps its result. StrictMode's\n * simulated unmount/remount in dev and a parent re-render with inline\n * callbacks used to cancel the only exchange and skip the second, leaving the\n * hook `\"pending\"` forever with the single-use code already spent. Now the\n * options live in a latest-value ref, the effect has no dependencies, and the\n * result is dropped only when the component is still unmounted when it lands.\n * The newest `onSuccess`/`onError` are the ones called.\n *\n * For a provider redirect, the exchange must carry the `state` along with the\n * `code`, or no backend can tell a forged callback from a real one (login\n * CSRF). `exchangeOAuthCallback` does that against a\n * `tempest-fastapi-sdk` backend; `readOAuthCallbackParams` returns both\n * values (or throws) for any other backend.\n *\n * @example\n * function OAuthCallback() {\n * const { loading, error } = useOAuthCallback<LoginResponse>({\n * exchange: () => exchangeOAuthCallback<LoginResponse>(api, \"google\"),\n * onSuccess: ({ user_id, access_token }) => {\n * useAuthStore.getState().setSession({ user: { id: user_id }, token: access_token });\n * navigate(\"/dashboard\", { replace: true });\n * },\n * onError: () => navigate(\"/login?error=oauth\", { replace: true }),\n * });\n *\n * if (loading) return <Spinner />;\n * if (error) return <ErrorState description=\"OAuth falhou\" />;\n * return null;\n * }\n */\nexport function useOAuthCallback<T>(\n options: UseOAuthCallbackOptions<T>,\n): UseOAuthCallbackResult<T> {\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [status, setStatus] = useState<\"pending\" | \"success\" | \"error\">(\"pending\");\n const optionsRef = useRef<UseOAuthCallbackOptions<T>>(options);\n const ranRef = useRef<boolean>(false);\n const mountedRef = useRef<boolean>(false);\n\n useEffect(() => {\n optionsRef.current = options;\n });\n\n useEffect(() => {\n mountedRef.current = true;\n if (!ranRef.current) {\n ranRef.current = true;\n optionsRef.current\n .exchange()\n .then((result) => {\n if (!mountedRef.current) return;\n setData(result);\n setStatus(\"success\");\n return optionsRef.current.onSuccess?.(result);\n })\n .catch((err: unknown) => {\n if (!mountedRef.current) return;\n setError(err);\n setStatus(\"error\");\n optionsRef.current.onError?.(err);\n });\n }\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n return { loading: status === \"pending\", data, error, status };\n}\n"],"mappings":"uBAyDA,SAAgB,EACZ,EACyB,CACzB,GAAM,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAmB,IAAI,EACzC,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAkB,IAAI,EAC1C,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA0C,SAAS,EACzE,GAAA,EAAa,EAAA,OAAA,CAAmC,CAAO,EACvD,GAAA,EAAS,EAAA,OAAA,CAAgB,EAAK,EAC9B,GAAA,EAAa,EAAA,OAAA,CAAgB,EAAK,EA8BxC,OA5BA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAW,QAAU,CACzB,CAAC,GAED,EAAA,EAAA,UAAA,MACI,EAAW,QAAU,GAChB,EAAO,UACR,EAAO,QAAU,GACjB,EAAW,QACN,SAAS,CAAC,CACV,KAAM,GAAW,CACT,KAAW,QAGhB,OAFA,EAAQ,CAAM,EACd,EAAU,SAAS,EACZ,EAAW,QAAQ,YAAY,CAAM,CAChD,CAAC,CAAC,CACD,MAAO,GAAiB,CAChB,EAAW,UAChB,EAAS,CAAG,EACZ,EAAU,OAAO,EACjB,EAAW,QAAQ,UAAU,CAAG,EACpC,CAAC,OAEI,CACT,EAAW,QAAU,EACzB,GACD,CAAC,CAAC,EAEE,CAAE,QAAS,IAAW,UAAW,OAAM,QAAO,QAAO,CAChE"}
@@ -1,27 +1,20 @@
1
1
  import { useEffect as e, useRef as t, useState as n } from "react";
2
2
  //#region src/oauth/use-oauth-callback.ts
3
3
  function r(r) {
4
- let { exchange: i, onSuccess: a, onError: o } = r, [s, c] = n(null), [l, u] = n(null), [d, f] = n("pending"), p = t(!1);
4
+ let [i, a] = n(null), [o, s] = n(null), [c, l] = n("pending"), u = t(r), d = t(!1), f = t(!1);
5
5
  return e(() => {
6
- if (p.current) return;
7
- p.current = !0;
8
- let e = !1;
9
- return i().then((t) => {
10
- if (!e) return c(t), f("success"), a?.(t);
11
- }).catch((t) => {
12
- e || (u(t), f("error"), o?.(t));
13
- }), () => {
14
- e = !0;
15
- };
16
- }, [
17
- i,
18
- a,
19
- o
20
- ]), {
21
- loading: d === "pending",
22
- data: s,
23
- error: l,
24
- status: d
6
+ u.current = r;
7
+ }), e(() => (f.current = !0, d.current || (d.current = !0, u.current.exchange().then((e) => {
8
+ if (f.current) return a(e), l("success"), u.current.onSuccess?.(e);
9
+ }).catch((e) => {
10
+ f.current && (s(e), l("error"), u.current.onError?.(e));
11
+ })), () => {
12
+ f.current = !1;
13
+ }), []), {
14
+ loading: c === "pending",
15
+ data: i,
16
+ error: o,
17
+ status: c
25
18
  };
26
19
  }
27
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-oauth-callback.js","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n * Survives React StrictMode double-mounting in dev — uses a ref guard to\n * ensure `exchange` runs once.\n *\n * @example\n * function OAuthCallback() {\n * const { loading, error } = useOAuthCallback({\n * exchange: async () => {\n * const code = new URLSearchParams(location.search).get(\"code\")!;\n * return api.post(\"/auth/google/exchange\", { body: { code } });\n * },\n * onSuccess: ({ token, user }) => {\n * useAuthStore.getState().setSession({ user, token });\n * navigate(\"/dashboard\", { replace: true });\n * },\n * onError: () => navigate(\"/login?error=oauth\", { replace: true }),\n * });\n *\n * if (loading) return <Spinner />;\n * if (error) return <ErrorState description=\"OAuth falhou\" />;\n * return null;\n * }\n */\nexport function useOAuthCallback<T>(\n options: UseOAuthCallbackOptions<T>,\n): UseOAuthCallbackResult<T> {\n const { exchange, onSuccess, onError } = options;\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [status, setStatus] = useState<\"pending\" | \"success\" | \"error\">(\"pending\");\n const ranRef = useRef<boolean>(false);\n\n useEffect(() => {\n if (ranRef.current) return;\n ranRef.current = true;\n let cancelled = false;\n exchange()\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setStatus(\"success\");\n return onSuccess?.(result);\n })\n .catch((err) => {\n if (cancelled) return;\n setError(err);\n setStatus(\"error\");\n onError?.(err);\n });\n return () => {\n cancelled = true;\n };\n }, [exchange, onSuccess, onError]);\n\n return { loading: status === \"pending\", data, error, status };\n}\n"],"mappings":";;AAiDA,SAAgB,EACZ,GACyB;CACzB,IAAM,EAAE,aAAU,cAAW,eAAY,GACnC,CAAC,GAAM,KAAW,EAAmB,IAAI,GACzC,CAAC,GAAO,KAAY,EAAkB,IAAI,GAC1C,CAAC,GAAQ,KAAa,EAA0C,SAAS,GACzE,IAAS,EAAgB,EAAK;CAwBpC,OAtBA,QAAgB;EACZ,IAAI,EAAO,SAAS;EACpB,EAAO,UAAU;EACjB,IAAI,IAAY;EAchB,OAbA,EAAS,CAAC,CACL,MAAM,MAAW;GACV,QAGJ,OAFA,EAAQ,CAAM,GACd,EAAU,SAAS,GACZ,IAAY,CAAM;EAC7B,CAAC,CAAC,CACD,OAAO,MAAQ;GACR,MACJ,EAAS,CAAG,GACZ,EAAU,OAAO,GACjB,IAAU,CAAG;EACjB,CAAC,SACQ;GACT,IAAY;EAChB;CACJ,GAAG;EAAC;EAAU;EAAW;CAAO,CAAC,GAE1B;EAAE,SAAS,MAAW;EAAW;EAAM;EAAO;CAAO;AAChE"}
1
+ {"version":3,"file":"use-oauth-callback.js","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n * Runs `exchange` once per logical mount and keeps its result. StrictMode's\n * simulated unmount/remount in dev and a parent re-render with inline\n * callbacks used to cancel the only exchange and skip the second, leaving the\n * hook `\"pending\"` forever with the single-use code already spent. Now the\n * options live in a latest-value ref, the effect has no dependencies, and the\n * result is dropped only when the component is still unmounted when it lands.\n * The newest `onSuccess`/`onError` are the ones called.\n *\n * For a provider redirect, the exchange must carry the `state` along with the\n * `code`, or no backend can tell a forged callback from a real one (login\n * CSRF). `exchangeOAuthCallback` does that against a\n * `tempest-fastapi-sdk` backend; `readOAuthCallbackParams` returns both\n * values (or throws) for any other backend.\n *\n * @example\n * function OAuthCallback() {\n * const { loading, error } = useOAuthCallback<LoginResponse>({\n * exchange: () => exchangeOAuthCallback<LoginResponse>(api, \"google\"),\n * onSuccess: ({ user_id, access_token }) => {\n * useAuthStore.getState().setSession({ user: { id: user_id }, token: access_token });\n * navigate(\"/dashboard\", { replace: true });\n * },\n * onError: () => navigate(\"/login?error=oauth\", { replace: true }),\n * });\n *\n * if (loading) return <Spinner />;\n * if (error) return <ErrorState description=\"OAuth falhou\" />;\n * return null;\n * }\n */\nexport function useOAuthCallback<T>(\n options: UseOAuthCallbackOptions<T>,\n): UseOAuthCallbackResult<T> {\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [status, setStatus] = useState<\"pending\" | \"success\" | \"error\">(\"pending\");\n const optionsRef = useRef<UseOAuthCallbackOptions<T>>(options);\n const ranRef = useRef<boolean>(false);\n const mountedRef = useRef<boolean>(false);\n\n useEffect(() => {\n optionsRef.current = options;\n });\n\n useEffect(() => {\n mountedRef.current = true;\n if (!ranRef.current) {\n ranRef.current = true;\n optionsRef.current\n .exchange()\n .then((result) => {\n if (!mountedRef.current) return;\n setData(result);\n setStatus(\"success\");\n return optionsRef.current.onSuccess?.(result);\n })\n .catch((err: unknown) => {\n if (!mountedRef.current) return;\n setError(err);\n setStatus(\"error\");\n optionsRef.current.onError?.(err);\n });\n }\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n return { loading: status === \"pending\", data, error, status };\n}\n"],"mappings":";;AAyDA,SAAgB,EACZ,GACyB;CACzB,IAAM,CAAC,GAAM,KAAW,EAAmB,IAAI,GACzC,CAAC,GAAO,KAAY,EAAkB,IAAI,GAC1C,CAAC,GAAQ,KAAa,EAA0C,SAAS,GACzE,IAAa,EAAmC,CAAO,GACvD,IAAS,EAAgB,EAAK,GAC9B,IAAa,EAAgB,EAAK;CA8BxC,OA5BA,QAAgB;EACZ,EAAW,UAAU;CACzB,CAAC,GAED,SACI,EAAW,UAAU,IAChB,EAAO,YACR,EAAO,UAAU,IACjB,EAAW,QACN,SAAS,CAAC,CACV,MAAM,MAAW;EACT,MAAW,SAGhB,OAFA,EAAQ,CAAM,GACd,EAAU,SAAS,GACZ,EAAW,QAAQ,YAAY,CAAM;CAChD,CAAC,CAAC,CACD,OAAO,MAAiB;EAChB,EAAW,YAChB,EAAS,CAAG,GACZ,EAAU,OAAO,GACjB,EAAW,QAAQ,UAAU,CAAG;CACpC,CAAC,UAEI;EACT,EAAW,UAAU;CACzB,IACD,CAAC,CAAC,GAEE;EAAE,SAAS,MAAW;EAAW;EAAM;EAAO;CAAO;AAChE"}
@@ -1,2 +1,2 @@
1
- const e=require("../hooks/use-latest-ref.cjs"),t=require("./utils.cjs"),n=require("./web-push-client.cjs");let r=require("react");function i(i){let a=e.useLatestRef(i),o=(0,r.useMemo)(()=>new n.WebPushClient({vapidPublicKey:i.vapidPublicKey,onSubscribe:e=>a.current.onSubscribe(e),onUnsubscribe:e=>a.current.onUnsubscribe?.(e),getRegistration:i.getRegistration}),[i.vapidPublicKey,i.getRegistration,a]),[s]=(0,r.useState)(()=>t.isPushSupported()),[c,l]=(0,r.useState)(()=>o.permission()),[u,d]=(0,r.useState)(!1),[f,p]=(0,r.useState)(!1),[m,h]=(0,r.useState)(null),g=(0,r.useCallback)(async()=>{if(s){l(o.permission());try{let e=await o.isSubscribed();d(e)}catch(e){h(e instanceof Error?e:Error(String(e)))}}},[o,s]);return(0,r.useEffect)(()=>{g()},[g]),{supported:s,permission:c,subscribed:u,loading:f,error:m,subscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.subscribe(),d(!0),l(o.permission())}catch(e){throw h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),unsubscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.unsubscribe(),d(!1)}catch(e){throw h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),refresh:g}}exports.usePushSubscription=i;
1
+ const e=require("../hooks/use-latest-ref.cjs"),t=require("./utils.cjs"),n=require("./web-push-client.cjs");let r=require("react");function i(i){let a=e.useLatestRef(i),o=(0,r.useMemo)(()=>new n.WebPushClient({vapidPublicKey:i.vapidPublicKey,onSubscribe:e=>a.current.onSubscribe(e),onUnsubscribe:e=>a.current.onUnsubscribe?.(e),getRegistration:i.getRegistration}),[i.vapidPublicKey,i.getRegistration,a]),[s]=(0,r.useState)(()=>t.isPushSupported()),[c,l]=(0,r.useState)(()=>o.permission()),[u,d]=(0,r.useState)(!1),[f,p]=(0,r.useState)(!1),[m,h]=(0,r.useState)(null),g=(0,r.useCallback)(async()=>{if(s){l(o.permission());try{let e=await o.isSubscribed();d(e)}catch(e){h(e instanceof Error?e:Error(String(e)))}}},[o,s]);return(0,r.useEffect)(()=>{g()},[g]),{supported:s,permission:c,subscribed:u,loading:f,error:m,subscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.subscribe(),d(!0),l(o.permission())}catch(e){throw h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),unsubscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.unsubscribe(),d(!1)}catch(e){throw e instanceof n.WebPushUnsubscribeError&&d(!e.browserUnsubscribed),h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),refresh:g}}exports.usePushSubscription=i;
2
2
  //# sourceMappingURL=use-push-subscription.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-push-subscription.cjs","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport { WebPushClient, type WebPushClientConfig } from \"./web-push-client\";\nimport { isPushSupported } from \"./utils\";\n\nexport interface UsePushSubscriptionResult {\n /** True when the browser exposes the Push + Notification APIs. */\n supported: boolean;\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission: NotificationPermission | \"unsupported\";\n /** True when a subscription is active on this device. */\n subscribed: boolean;\n /** True while a subscribe/unsubscribe call is in-flight. */\n loading: boolean;\n /** Last error thrown by subscribe/unsubscribe, if any. */\n error: Error | null;\n subscribe: () => Promise<void>;\n unsubscribe: () => Promise<void>;\n /** Re-read the current subscription/permission state from the browser. */\n refresh: () => Promise<void>;\n}\n\n/**\n * React hook around {@link WebPushClient}. Tracks subscription, permission and\n * loading state; exposes imperative `subscribe`/`unsubscribe` actions.\n *\n * The hook does NOT register the service worker — the host app must do that\n * (e.g. via `vite-plugin-pwa` or a manual `navigator.serviceWorker.register`).\n *\n * @example\n * const push = usePushSubscription({\n * vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,\n * onSubscribe: (sub) => api.post(\"/webpush/subscribe\", { body: sub }),\n * onUnsubscribe: () => api.delete(\"/webpush/my\"),\n * });\n */\nexport function usePushSubscription(config: WebPushClientConfig): UsePushSubscriptionResult {\n const configRef = useLatestRef(config);\n\n const client = useMemo(\n () =>\n new WebPushClient({\n vapidPublicKey: config.vapidPublicKey,\n onSubscribe: (sub) => configRef.current.onSubscribe(sub),\n onUnsubscribe: (sub) => configRef.current.onUnsubscribe?.(sub),\n getRegistration: config.getRegistration,\n }),\n [config.vapidPublicKey, config.getRegistration, configRef],\n );\n\n const [supported] = useState<boolean>(() => isPushSupported());\n const [permission, setPermission] = useState<NotificationPermission | \"unsupported\">(() =>\n client.permission(),\n );\n const [subscribed, setSubscribed] = useState<boolean>(false);\n const [loading, setLoading] = useState<boolean>(false);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n if (!supported) return;\n setPermission(client.permission());\n try {\n const active = await client.isSubscribed();\n setSubscribed(active);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [client, supported]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const subscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.subscribe();\n setSubscribed(true);\n setPermission(client.permission());\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n const unsubscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.unsubscribe();\n setSubscribed(false);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n return {\n supported,\n permission,\n subscribed,\n loading,\n error,\n subscribe,\n unsubscribe,\n refresh,\n };\n}\n"],"mappings":"kIAqCA,SAAgB,EAAoB,EAAwD,CACxF,IAAM,EAAY,EAAA,aAAa,CAAM,EAE/B,GAAA,EAAS,EAAA,QAAA,KAEP,IAAI,EAAA,cAAc,CACd,eAAgB,EAAO,eACvB,YAAc,GAAQ,EAAU,QAAQ,YAAY,CAAG,EACvD,cAAgB,GAAQ,EAAU,QAAQ,gBAAgB,CAAG,EAC7D,gBAAiB,EAAO,eAC5B,CAAC,EACL,CAAC,EAAO,eAAgB,EAAO,gBAAiB,CAAS,CAC7D,EAEM,CAAC,IAAA,EAAa,EAAA,SAAA,KAAwB,EAAA,gBAAgB,CAAC,EACvD,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,KAChC,EAAO,WAAW,CACtB,EACM,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAkB,EAAK,EACrD,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAkB,EAAK,EAC/C,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAuB,IAAI,EAE/C,GAAA,EAAU,EAAA,YAAA,CAAY,SAAY,CAC/B,KACL,GAAc,EAAO,WAAW,CAAC,EACjC,GAAI,CACA,IAAM,EAAS,MAAM,EAAO,aAAa,EACzC,EAAc,CAAM,CACxB,OAAS,EAAK,CACV,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,CAChE,CANiC,CAOrC,EAAG,CAAC,EAAQ,CAAS,CAAC,EAqCtB,OAnCA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAa,CACjB,EAAG,CAAC,CAAO,CAAC,EAiCL,CACH,YACA,aACA,aACA,UACA,QACA,WAAA,EArCc,EAAA,YAAA,CAAY,SAAY,CACjC,KAEL,CADA,EAAW,EAAI,EACf,EAAS,IAAI,EACb,GAAI,CACA,MAAM,EAAO,UAAU,EACvB,EAAc,EAAI,EAClB,EAAc,EAAO,WAAW,CAAC,CACrC,OAAS,EAAK,CAEV,MADA,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,EACtD,CACV,QAAU,CACN,EAAW,EAAK,CACpB,CAVa,CAWjB,EAAG,CAAC,EAAQ,CAAS,CAuBjB,EACA,aAAA,EAtBgB,EAAA,YAAA,CAAY,SAAY,CACnC,KAEL,CADA,EAAW,EAAI,EACf,EAAS,IAAI,EACb,GAAI,CACA,MAAM,EAAO,YAAY,EACzB,EAAc,EAAK,CACvB,OAAS,EAAK,CAEV,MADA,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,EACtD,CACV,QAAU,CACN,EAAW,EAAK,CACpB,CATa,CAUjB,EAAG,CAAC,EAAQ,CAAS,CASjB,EACA,SACJ,CACJ"}
1
+ {"version":3,"file":"use-push-subscription.cjs","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n WebPushClient,\n WebPushUnsubscribeError,\n type WebPushClientConfig,\n} from \"./web-push-client\";\nimport { isPushSupported } from \"./utils\";\n\nexport interface UsePushSubscriptionResult {\n /** True when the browser exposes the Push + Notification APIs. */\n supported: boolean;\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission: NotificationPermission | \"unsupported\";\n /** True when a subscription is active on this device. */\n subscribed: boolean;\n /** True while a subscribe/unsubscribe call is in-flight. */\n loading: boolean;\n /** Last error thrown by subscribe/unsubscribe, if any. */\n error: Error | null;\n subscribe: () => Promise<void>;\n unsubscribe: () => Promise<void>;\n /** Re-read the current subscription/permission state from the browser. */\n refresh: () => Promise<void>;\n}\n\n/**\n * React hook around {@link WebPushClient}. Tracks subscription, permission and\n * loading state; exposes imperative `subscribe`/`unsubscribe` actions.\n *\n * The hook does NOT register the service worker — the host app must do that\n * (e.g. via `vite-plugin-pwa` or a manual `navigator.serviceWorker.register`).\n *\n * @example\n * const push = usePushSubscription({\n * vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,\n * onSubscribe: (sub) => api.post(\"/webpush/subscribe\", { body: sub }),\n * onUnsubscribe: () => api.delete(\"/webpush/my\"),\n * });\n */\nexport function usePushSubscription(config: WebPushClientConfig): UsePushSubscriptionResult {\n const configRef = useLatestRef(config);\n\n const client = useMemo(\n () =>\n new WebPushClient({\n vapidPublicKey: config.vapidPublicKey,\n onSubscribe: (sub) => configRef.current.onSubscribe(sub),\n onUnsubscribe: (sub) => configRef.current.onUnsubscribe?.(sub),\n getRegistration: config.getRegistration,\n }),\n [config.vapidPublicKey, config.getRegistration, configRef],\n );\n\n const [supported] = useState<boolean>(() => isPushSupported());\n const [permission, setPermission] = useState<NotificationPermission | \"unsupported\">(() =>\n client.permission(),\n );\n const [subscribed, setSubscribed] = useState<boolean>(false);\n const [loading, setLoading] = useState<boolean>(false);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n if (!supported) return;\n setPermission(client.permission());\n try {\n const active = await client.isSubscribed();\n setSubscribed(active);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [client, supported]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const subscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.subscribe();\n setSubscribed(true);\n setPermission(client.permission());\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n const unsubscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.unsubscribe();\n setSubscribed(false);\n } catch (err) {\n if (err instanceof WebPushUnsubscribeError) setSubscribed(!err.browserUnsubscribed);\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n return {\n supported,\n permission,\n subscribed,\n loading,\n error,\n subscribe,\n unsubscribe,\n refresh,\n };\n}\n"],"mappings":"kIAyCA,SAAgB,EAAoB,EAAwD,CACxF,IAAM,EAAY,EAAA,aAAa,CAAM,EAE/B,GAAA,EAAS,EAAA,QAAA,KAEP,IAAI,EAAA,cAAc,CACd,eAAgB,EAAO,eACvB,YAAc,GAAQ,EAAU,QAAQ,YAAY,CAAG,EACvD,cAAgB,GAAQ,EAAU,QAAQ,gBAAgB,CAAG,EAC7D,gBAAiB,EAAO,eAC5B,CAAC,EACL,CAAC,EAAO,eAAgB,EAAO,gBAAiB,CAAS,CAC7D,EAEM,CAAC,IAAA,EAAa,EAAA,SAAA,KAAwB,EAAA,gBAAgB,CAAC,EACvD,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,KAChC,EAAO,WAAW,CACtB,EACM,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAkB,EAAK,EACrD,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAkB,EAAK,EAC/C,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAuB,IAAI,EAE/C,GAAA,EAAU,EAAA,YAAA,CAAY,SAAY,CAC/B,KACL,GAAc,EAAO,WAAW,CAAC,EACjC,GAAI,CACA,IAAM,EAAS,MAAM,EAAO,aAAa,EACzC,EAAc,CAAM,CACxB,OAAS,EAAK,CACV,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,CAChE,CANiC,CAOrC,EAAG,CAAC,EAAQ,CAAS,CAAC,EAsCtB,OApCA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAa,CACjB,EAAG,CAAC,CAAO,CAAC,EAkCL,CACH,YACA,aACA,aACA,UACA,QACA,WAAA,EAtCc,EAAA,YAAA,CAAY,SAAY,CACjC,KAEL,CADA,EAAW,EAAI,EACf,EAAS,IAAI,EACb,GAAI,CACA,MAAM,EAAO,UAAU,EACvB,EAAc,EAAI,EAClB,EAAc,EAAO,WAAW,CAAC,CACrC,OAAS,EAAK,CAEV,MADA,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,EACtD,CACV,QAAU,CACN,EAAW,EAAK,CACpB,CAVa,CAWjB,EAAG,CAAC,EAAQ,CAAS,CAwBjB,EACA,aAAA,EAvBgB,EAAA,YAAA,CAAY,SAAY,CACnC,KAEL,CADA,EAAW,EAAI,EACf,EAAS,IAAI,EACb,GAAI,CACA,MAAM,EAAO,YAAY,EACzB,EAAc,EAAK,CACvB,OAAS,EAAK,CAGV,MAFI,aAAe,EAAA,yBAAyB,EAAc,CAAC,EAAI,mBAAmB,EAClF,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,EACtD,CACV,QAAU,CACN,EAAW,EAAK,CACpB,CAVa,CAWjB,EAAG,CAAC,EAAQ,CAAS,CASjB,EACA,SACJ,CACJ"}
@@ -1,65 +1,65 @@
1
1
  import { useLatestRef as e } from "../hooks/use-latest-ref.js";
2
2
  import { isPushSupported as t } from "./utils.js";
3
- import { WebPushClient as n } from "./web-push-client.js";
4
- import { useCallback as r, useEffect as i, useMemo as a, useState as o } from "react";
3
+ import { WebPushClient as n, WebPushUnsubscribeError as r } from "./web-push-client.js";
4
+ import { useCallback as i, useEffect as a, useMemo as o, useState as s } from "react";
5
5
  //#region src/push/use-push-subscription.ts
6
- function s(s) {
7
- let c = e(s), l = a(() => new n({
8
- vapidPublicKey: s.vapidPublicKey,
9
- onSubscribe: (e) => c.current.onSubscribe(e),
10
- onUnsubscribe: (e) => c.current.onUnsubscribe?.(e),
11
- getRegistration: s.getRegistration
6
+ function c(c) {
7
+ let l = e(c), u = o(() => new n({
8
+ vapidPublicKey: c.vapidPublicKey,
9
+ onSubscribe: (e) => l.current.onSubscribe(e),
10
+ onUnsubscribe: (e) => l.current.onUnsubscribe?.(e),
11
+ getRegistration: c.getRegistration
12
12
  }), [
13
- s.vapidPublicKey,
14
- s.getRegistration,
15
- c
16
- ]), [u] = o(() => t()), [d, f] = o(() => l.permission()), [p, m] = o(!1), [h, g] = o(!1), [_, v] = o(null), y = r(async () => {
17
- if (u) {
18
- f(l.permission());
13
+ c.vapidPublicKey,
14
+ c.getRegistration,
15
+ l
16
+ ]), [d] = s(() => t()), [f, p] = s(() => u.permission()), [m, h] = s(!1), [g, _] = s(!1), [v, y] = s(null), b = i(async () => {
17
+ if (d) {
18
+ p(u.permission());
19
19
  try {
20
- let e = await l.isSubscribed();
21
- m(e);
20
+ let e = await u.isSubscribed();
21
+ h(e);
22
22
  } catch (e) {
23
- v(e instanceof Error ? e : Error(String(e)));
23
+ y(e instanceof Error ? e : Error(String(e)));
24
24
  }
25
25
  }
26
- }, [l, u]);
27
- return i(() => {
28
- y();
29
- }, [y]), {
30
- supported: u,
31
- permission: d,
32
- subscribed: p,
33
- loading: h,
34
- error: _,
35
- subscribe: r(async () => {
36
- if (u) {
37
- g(!0), v(null);
26
+ }, [u, d]);
27
+ return a(() => {
28
+ b();
29
+ }, [b]), {
30
+ supported: d,
31
+ permission: f,
32
+ subscribed: m,
33
+ loading: g,
34
+ error: v,
35
+ subscribe: i(async () => {
36
+ if (d) {
37
+ _(!0), y(null);
38
38
  try {
39
- await l.subscribe(), m(!0), f(l.permission());
39
+ await u.subscribe(), h(!0), p(u.permission());
40
40
  } catch (e) {
41
- throw v(e instanceof Error ? e : Error(String(e))), e;
41
+ throw y(e instanceof Error ? e : Error(String(e))), e;
42
42
  } finally {
43
- g(!1);
43
+ _(!1);
44
44
  }
45
45
  }
46
- }, [l, u]),
47
- unsubscribe: r(async () => {
48
- if (u) {
49
- g(!0), v(null);
46
+ }, [u, d]),
47
+ unsubscribe: i(async () => {
48
+ if (d) {
49
+ _(!0), y(null);
50
50
  try {
51
- await l.unsubscribe(), m(!1);
51
+ await u.unsubscribe(), h(!1);
52
52
  } catch (e) {
53
- throw v(e instanceof Error ? e : Error(String(e))), e;
53
+ throw e instanceof r && h(!e.browserUnsubscribed), y(e instanceof Error ? e : Error(String(e))), e;
54
54
  } finally {
55
- g(!1);
55
+ _(!1);
56
56
  }
57
57
  }
58
- }, [l, u]),
59
- refresh: y
58
+ }, [u, d]),
59
+ refresh: b
60
60
  };
61
61
  }
62
62
  //#endregion
63
- export { s as usePushSubscription };
63
+ export { c as usePushSubscription };
64
64
 
65
65
  //# sourceMappingURL=use-push-subscription.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-push-subscription.js","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport { WebPushClient, type WebPushClientConfig } from \"./web-push-client\";\nimport { isPushSupported } from \"./utils\";\n\nexport interface UsePushSubscriptionResult {\n /** True when the browser exposes the Push + Notification APIs. */\n supported: boolean;\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission: NotificationPermission | \"unsupported\";\n /** True when a subscription is active on this device. */\n subscribed: boolean;\n /** True while a subscribe/unsubscribe call is in-flight. */\n loading: boolean;\n /** Last error thrown by subscribe/unsubscribe, if any. */\n error: Error | null;\n subscribe: () => Promise<void>;\n unsubscribe: () => Promise<void>;\n /** Re-read the current subscription/permission state from the browser. */\n refresh: () => Promise<void>;\n}\n\n/**\n * React hook around {@link WebPushClient}. Tracks subscription, permission and\n * loading state; exposes imperative `subscribe`/`unsubscribe` actions.\n *\n * The hook does NOT register the service worker — the host app must do that\n * (e.g. via `vite-plugin-pwa` or a manual `navigator.serviceWorker.register`).\n *\n * @example\n * const push = usePushSubscription({\n * vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,\n * onSubscribe: (sub) => api.post(\"/webpush/subscribe\", { body: sub }),\n * onUnsubscribe: () => api.delete(\"/webpush/my\"),\n * });\n */\nexport function usePushSubscription(config: WebPushClientConfig): UsePushSubscriptionResult {\n const configRef = useLatestRef(config);\n\n const client = useMemo(\n () =>\n new WebPushClient({\n vapidPublicKey: config.vapidPublicKey,\n onSubscribe: (sub) => configRef.current.onSubscribe(sub),\n onUnsubscribe: (sub) => configRef.current.onUnsubscribe?.(sub),\n getRegistration: config.getRegistration,\n }),\n [config.vapidPublicKey, config.getRegistration, configRef],\n );\n\n const [supported] = useState<boolean>(() => isPushSupported());\n const [permission, setPermission] = useState<NotificationPermission | \"unsupported\">(() =>\n client.permission(),\n );\n const [subscribed, setSubscribed] = useState<boolean>(false);\n const [loading, setLoading] = useState<boolean>(false);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n if (!supported) return;\n setPermission(client.permission());\n try {\n const active = await client.isSubscribed();\n setSubscribed(active);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [client, supported]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const subscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.subscribe();\n setSubscribed(true);\n setPermission(client.permission());\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n const unsubscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.unsubscribe();\n setSubscribed(false);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n return {\n supported,\n permission,\n subscribed,\n loading,\n error,\n subscribe,\n unsubscribe,\n refresh,\n };\n}\n"],"mappings":";;;;;AAqCA,SAAgB,EAAoB,GAAwD;CACxF,IAAM,IAAY,EAAa,CAAM,GAE/B,IAAS,QAEP,IAAI,EAAc;EACd,gBAAgB,EAAO;EACvB,cAAc,MAAQ,EAAU,QAAQ,YAAY,CAAG;EACvD,gBAAgB,MAAQ,EAAU,QAAQ,gBAAgB,CAAG;EAC7D,iBAAiB,EAAO;CAC5B,CAAC,GACL;EAAC,EAAO;EAAgB,EAAO;EAAiB;CAAS,CAC7D,GAEM,CAAC,KAAa,QAAwB,EAAgB,CAAC,GACvD,CAAC,GAAY,KAAiB,QAChC,EAAO,WAAW,CACtB,GACM,CAAC,GAAY,KAAiB,EAAkB,EAAK,GACrD,CAAC,GAAS,KAAc,EAAkB,EAAK,GAC/C,CAAC,GAAO,KAAY,EAAuB,IAAI,GAE/C,IAAU,EAAY,YAAY;EAC/B,OACL;KAAc,EAAO,WAAW,CAAC;GACjC,IAAI;IACA,IAAM,IAAS,MAAM,EAAO,aAAa;IACzC,EAAc,CAAM;GACxB,SAAS,GAAK;IACV,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC;GAChE;EANiC;CAOrC,GAAG,CAAC,GAAQ,CAAS,CAAC;CAqCtB,OAnCA,QAAgB;EACZ,EAAa;CACjB,GAAG,CAAC,CAAO,CAAC,GAiCL;EACH;EACA;EACA;EACA;EACA;EACA,WArCc,EAAY,YAAY;GACjC,OAEL;IADA,EAAW,EAAI,GACf,EAAS,IAAI;IACb,IAAI;KAGA,AAFA,MAAM,EAAO,UAAU,GACvB,EAAc,EAAI,GAClB,EAAc,EAAO,WAAW,CAAC;IACrC,SAAS,GAAK;KAEV,MADA,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC,GACtD;IACV,UAAU;KACN,EAAW,EAAK;IACpB;GAVa;EAWjB,GAAG,CAAC,GAAQ,CAAS,CAuBjB;EACA,aAtBgB,EAAY,YAAY;GACnC,OAEL;IADA,EAAW,EAAI,GACf,EAAS,IAAI;IACb,IAAI;KAEA,AADA,MAAM,EAAO,YAAY,GACzB,EAAc,EAAK;IACvB,SAAS,GAAK;KAEV,MADA,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC,GACtD;IACV,UAAU;KACN,EAAW,EAAK;IACpB;GATa;EAUjB,GAAG,CAAC,GAAQ,CAAS,CASjB;EACA;CACJ;AACJ"}
1
+ {"version":3,"file":"use-push-subscription.js","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n WebPushClient,\n WebPushUnsubscribeError,\n type WebPushClientConfig,\n} from \"./web-push-client\";\nimport { isPushSupported } from \"./utils\";\n\nexport interface UsePushSubscriptionResult {\n /** True when the browser exposes the Push + Notification APIs. */\n supported: boolean;\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission: NotificationPermission | \"unsupported\";\n /** True when a subscription is active on this device. */\n subscribed: boolean;\n /** True while a subscribe/unsubscribe call is in-flight. */\n loading: boolean;\n /** Last error thrown by subscribe/unsubscribe, if any. */\n error: Error | null;\n subscribe: () => Promise<void>;\n unsubscribe: () => Promise<void>;\n /** Re-read the current subscription/permission state from the browser. */\n refresh: () => Promise<void>;\n}\n\n/**\n * React hook around {@link WebPushClient}. Tracks subscription, permission and\n * loading state; exposes imperative `subscribe`/`unsubscribe` actions.\n *\n * The hook does NOT register the service worker — the host app must do that\n * (e.g. via `vite-plugin-pwa` or a manual `navigator.serviceWorker.register`).\n *\n * @example\n * const push = usePushSubscription({\n * vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,\n * onSubscribe: (sub) => api.post(\"/webpush/subscribe\", { body: sub }),\n * onUnsubscribe: () => api.delete(\"/webpush/my\"),\n * });\n */\nexport function usePushSubscription(config: WebPushClientConfig): UsePushSubscriptionResult {\n const configRef = useLatestRef(config);\n\n const client = useMemo(\n () =>\n new WebPushClient({\n vapidPublicKey: config.vapidPublicKey,\n onSubscribe: (sub) => configRef.current.onSubscribe(sub),\n onUnsubscribe: (sub) => configRef.current.onUnsubscribe?.(sub),\n getRegistration: config.getRegistration,\n }),\n [config.vapidPublicKey, config.getRegistration, configRef],\n );\n\n const [supported] = useState<boolean>(() => isPushSupported());\n const [permission, setPermission] = useState<NotificationPermission | \"unsupported\">(() =>\n client.permission(),\n );\n const [subscribed, setSubscribed] = useState<boolean>(false);\n const [loading, setLoading] = useState<boolean>(false);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n if (!supported) return;\n setPermission(client.permission());\n try {\n const active = await client.isSubscribed();\n setSubscribed(active);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [client, supported]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const subscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.subscribe();\n setSubscribed(true);\n setPermission(client.permission());\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n const unsubscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.unsubscribe();\n setSubscribed(false);\n } catch (err) {\n if (err instanceof WebPushUnsubscribeError) setSubscribed(!err.browserUnsubscribed);\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n return {\n supported,\n permission,\n subscribed,\n loading,\n error,\n subscribe,\n unsubscribe,\n refresh,\n };\n}\n"],"mappings":";;;;;AAyCA,SAAgB,EAAoB,GAAwD;CACxF,IAAM,IAAY,EAAa,CAAM,GAE/B,IAAS,QAEP,IAAI,EAAc;EACd,gBAAgB,EAAO;EACvB,cAAc,MAAQ,EAAU,QAAQ,YAAY,CAAG;EACvD,gBAAgB,MAAQ,EAAU,QAAQ,gBAAgB,CAAG;EAC7D,iBAAiB,EAAO;CAC5B,CAAC,GACL;EAAC,EAAO;EAAgB,EAAO;EAAiB;CAAS,CAC7D,GAEM,CAAC,KAAa,QAAwB,EAAgB,CAAC,GACvD,CAAC,GAAY,KAAiB,QAChC,EAAO,WAAW,CACtB,GACM,CAAC,GAAY,KAAiB,EAAkB,EAAK,GACrD,CAAC,GAAS,KAAc,EAAkB,EAAK,GAC/C,CAAC,GAAO,KAAY,EAAuB,IAAI,GAE/C,IAAU,EAAY,YAAY;EAC/B,OACL;KAAc,EAAO,WAAW,CAAC;GACjC,IAAI;IACA,IAAM,IAAS,MAAM,EAAO,aAAa;IACzC,EAAc,CAAM;GACxB,SAAS,GAAK;IACV,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC;GAChE;EANiC;CAOrC,GAAG,CAAC,GAAQ,CAAS,CAAC;CAsCtB,OApCA,QAAgB;EACZ,EAAa;CACjB,GAAG,CAAC,CAAO,CAAC,GAkCL;EACH;EACA;EACA;EACA;EACA;EACA,WAtCc,EAAY,YAAY;GACjC,OAEL;IADA,EAAW,EAAI,GACf,EAAS,IAAI;IACb,IAAI;KAGA,AAFA,MAAM,EAAO,UAAU,GACvB,EAAc,EAAI,GAClB,EAAc,EAAO,WAAW,CAAC;IACrC,SAAS,GAAK;KAEV,MADA,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC,GACtD;IACV,UAAU;KACN,EAAW,EAAK;IACpB;GAVa;EAWjB,GAAG,CAAC,GAAQ,CAAS,CAwBjB;EACA,aAvBgB,EAAY,YAAY;GACnC,OAEL;IADA,EAAW,EAAI,GACf,EAAS,IAAI;IACb,IAAI;KAEA,AADA,MAAM,EAAO,YAAY,GACzB,EAAc,EAAK;IACvB,SAAS,GAAK;KAGV,MAFI,aAAe,KAAyB,EAAc,CAAC,EAAI,mBAAmB,GAClF,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC,GACtD;IACV,UAAU;KACN,EAAW,EAAK;IACpB;GAVa;EAWjB,GAAG,CAAC,GAAQ,CAAS,CASjB;EACA;CACJ;AACJ"}
@@ -1,2 +1,2 @@
1
- const e=require("./utils.cjs");var t=class extends Error{constructor(){super(`Web Push não suportado neste navegador.`),this.name=`WebPushUnsupportedError`}},n=class extends Error{constructor(){super(`Permissão de notificação negada pelo usuário.`),this.name=`WebPushPermissionDeniedError`}},r=class{config;constructor(e){this.config=e}static isSupported(){return e.isPushSupported()}async registration(){if(!e.isPushSupported())throw new t;return this.config.getRegistration?await this.config.getRegistration():await navigator.serviceWorker.ready}permission(){return typeof Notification>`u`?`unsupported`:Notification.permission}async requestPermission(){if(typeof Notification>`u`)throw new t;return Notification.requestPermission()}async getSubscription(){return(await this.registration()).pushManager.getSubscription()}async isSubscribed(){return e.isPushSupported()?await this.getSubscription()!==null:!1}async subscribe(){if(await this.requestPermission()!==`granted`)throw new n;let t=await this.registration(),r=await t.pushManager.getSubscription();if(r)return await this.config.onSubscribe(r.toJSON()),r;let i=await t.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:e.urlBase64ToUint8Array(this.config.vapidPublicKey)});return await this.config.onSubscribe(i.toJSON()),i}async unsubscribe(){let e=await this.getSubscription();return e?(this.config.onUnsubscribe&&await this.config.onUnsubscribe(e.toJSON()),e.unsubscribe()):!1}};exports.WebPushClient=r,exports.WebPushPermissionDeniedError=n,exports.WebPushUnsupportedError=t;
1
+ const e=require("./utils.cjs");var t=class extends Error{constructor(){super(`Web Push não suportado neste navegador.`),this.name=`WebPushUnsupportedError`}},n=class extends Error{constructor(){super(`Permissão de notificação negada pelo usuário.`),this.name=`WebPushPermissionDeniedError`}},r=class extends Error{subscription;browserUnsubscribed;constructor(e,t,n){super(`Falha ao remover a inscrição de push no backend.`,{cause:n}),this.name=`WebPushUnsubscribeError`,this.subscription=e,this.browserUnsubscribed=t}},i=class{config;constructor(e){this.config=e}static isSupported(){return e.isPushSupported()}async registration(){if(!e.isPushSupported())throw new t;return this.config.getRegistration?await this.config.getRegistration():await navigator.serviceWorker.ready}permission(){return typeof Notification>`u`?`unsupported`:Notification.permission}async requestPermission(){if(typeof Notification>`u`)throw new t;return Notification.requestPermission()}async getSubscription(){return(await this.registration()).pushManager.getSubscription()}async isSubscribed(){return e.isPushSupported()?await this.getSubscription()!==null:!1}async subscribe(){if(await this.requestPermission()!==`granted`)throw new n;let t=await this.registration(),r=await t.pushManager.getSubscription();if(r)return await this.config.onSubscribe(r.toJSON()),r;let i=await t.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:e.urlBase64ToUint8Array(this.config.vapidPublicKey)});return await this.config.onSubscribe(i.toJSON()),i}async unsubscribe(){let e=await this.getSubscription();if(!e)return!1;let t=e.toJSON(),n=null;if(this.config.onUnsubscribe)try{await this.config.onUnsubscribe(t)}catch(e){n={cause:e}}if(!n)return e.unsubscribe();let i;try{i=await e.unsubscribe()}catch(e){throw new r(t,!1,AggregateError([n.cause,e],`Backend delete and browser unsubscribe both failed.`))}throw new r(t,i,n.cause)}};exports.WebPushClient=i,exports.WebPushPermissionDeniedError=n,exports.WebPushUnsubscribeError=r,exports.WebPushUnsupportedError=t;
2
2
  //# sourceMappingURL=web-push-client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-push-client.cjs","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /** Remove the subscription from the backend. Called before `subscription.unsubscribe()`. */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription. Calls `onUnsubscribe` first (so the\n * backend can purge the record) and then `subscription.unsubscribe()`.\n *\n * @returns True when an active subscription was removed, false otherwise.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n if (this.config.onUnsubscribe) {\n await this.config.onUnsubscribe(subscription.toJSON());\n }\n return subscription.unsubscribe();\n }\n}\n"],"mappings":"+BAoBA,IAAa,EAAb,cAA6C,KAAM,CAC/C,aAAc,CACV,MAAM,yCAAyC,EAC/C,KAAK,KAAO,yBAChB,CACJ,EAEa,EAAb,cAAkD,KAAM,CACpD,aAAc,CACV,MAAM,+CAA+C,EACrD,KAAK,KAAO,8BAChB,CACJ,EAOa,EAAb,KAA2B,CACvB,OAEA,YAAY,EAA6B,CACrC,KAAK,OAAS,CAClB,CAGA,OAAO,aAAuB,CAC1B,OAAO,EAAA,gBAAgB,CAC3B,CAEA,MAAc,cAAmD,CAC7D,GAAI,CAAC,EAAA,gBAAgB,EAAG,MAAM,IAAI,EAClC,OAAO,KAAK,OAAO,gBACb,MAAM,KAAK,OAAO,gBAAgB,EAClC,MAAM,UAAU,cAAc,KACxC,CAGA,YAAqD,CAEjD,OADI,OAAO,aAAiB,IAAoB,cACzC,aAAa,UACxB,CAQA,MAAM,mBAAqD,CACvD,GAAI,OAAO,aAAiB,IAAa,MAAM,IAAI,EACnD,OAAO,aAAa,kBAAkB,CAC1C,CAGA,MAAM,iBAAoD,CAEtD,OAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB,CACpD,CAGA,MAAM,cAAiC,CAGnC,OAFK,EAAA,gBAAgB,EAEd,MADgB,KAAK,gBAAgB,IACxB,KAFW,EAGnC,CAWA,MAAM,WAAuC,CAEzC,GAAI,MADqB,KAAK,kBAAkB,IAC7B,UAAW,MAAM,IAAI,EAExC,IAAM,EAAe,MAAM,KAAK,aAAa,EACvC,EAAW,MAAM,EAAa,YAAY,gBAAgB,EAChE,GAAI,EAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,EACxC,EAGX,IAAM,EAAe,MAAM,EAAa,YAAY,UAAU,CAC1D,gBAAiB,GACjB,qBAAsB,EAAA,sBAAsB,KAAK,OAAO,cAAc,CAC1E,CAAC,EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,EAC5C,CACX,CAQA,MAAM,aAAgC,CAClC,IAAM,EAAe,MAAM,KAAK,gBAAgB,EAKhD,OAJK,GACD,KAAK,OAAO,eACZ,MAAM,KAAK,OAAO,cAAc,EAAa,OAAO,CAAC,EAElD,EAAa,YAAY,GAJN,EAK9B,CACJ"}
1
+ {"version":3,"file":"web-push-client.cjs","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Remove the subscription from the backend. Called before\n * `subscription.unsubscribe()`, with the subscription's JSON — key the\n * delete by its `endpoint`. A rejection no longer keeps the browser\n * subscribed: see {@link WebPushClient.unsubscribe}.\n */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Thrown by {@link WebPushClient.unsubscribe} when `onUnsubscribe` failed. The\n * browser subscription was still released — `browserUnsubscribed` says whether\n * the browser confirmed it — so the only thing left behind is the backend row.\n *\n * Carries the `subscription` JSON so the app can retry the delete by\n * `endpoint` later; if it never does, the push service answers the next send\n * to that endpoint with `404`/`410`, which is the backend's cue to drop the row.\n */\nexport class WebPushUnsubscribeError extends Error {\n /** The subscription whose backend delete failed. */\n readonly subscription: PushSubscriptionJSON;\n /** Whether `PushSubscription.unsubscribe()` reported success. */\n readonly browserUnsubscribed: boolean;\n\n /**\n * @param subscription - The subscription whose backend delete failed.\n * @param browserUnsubscribed - What `PushSubscription.unsubscribe()` returned.\n * @param cause - What `onUnsubscribe` threw.\n */\n constructor(subscription: PushSubscriptionJSON, browserUnsubscribed: boolean, cause: unknown) {\n super(\"Falha ao remover a inscrição de push no backend.\", { cause });\n this.name = \"WebPushUnsubscribeError\";\n this.subscription = subscription;\n this.browserUnsubscribed = browserUnsubscribed;\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription: `onUnsubscribe` first, so the backend\n * can purge the record while the endpoint is known, then\n * `subscription.unsubscribe()` — **always**, even when `onUnsubscribe`\n * failed.\n *\n * Why the browser is released anyway: the two failure modes are not\n * symmetric. A backend row left behind costs one send to a dead endpoint,\n * which the push service answers with `404`/`410` and the backend deletes.\n * A browser subscription left behind keeps delivering this account's\n * notifications to whoever uses the browser next — and the backend call\n * fails precisely when that matters, at the end of a session, when the\n * token that authenticates it is already gone. Before 0.77.0 a rejected\n * `onUnsubscribe` aborted here and the browser stayed subscribed (#445).\n * The endpoint is not lost either way: it is read before both calls and\n * travels in the error.\n *\n * @returns True when an active subscription was removed, false when there\n * was none or the browser refused.\n * @throws {WebPushUnsubscribeError} When `onUnsubscribe` failed — thrown\n * after the browser subscription was released. When the browser call also\n * threw, `browserUnsubscribed` is `false` and `cause` is an\n * `AggregateError` holding both errors, backend first, so the endpoint the\n * error carries is never traded for a bare `DOMException`.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n const json = subscription.toJSON();\n let backendError: { cause: unknown } | null = null;\n if (this.config.onUnsubscribe) {\n try {\n await this.config.onUnsubscribe(json);\n } catch (cause) {\n backendError = { cause };\n }\n }\n if (!backendError) return subscription.unsubscribe();\n let released: boolean;\n try {\n released = await subscription.unsubscribe();\n } catch (browserError) {\n throw new WebPushUnsubscribeError(\n json,\n false,\n new AggregateError(\n [backendError.cause, browserError],\n \"Backend delete and browser unsubscribe both failed.\",\n ),\n );\n }\n throw new WebPushUnsubscribeError(json, released, backendError.cause);\n }\n}\n"],"mappings":"+BAyBA,IAAa,EAAb,cAA6C,KAAM,CAC/C,aAAc,CACV,MAAM,yCAAyC,EAC/C,KAAK,KAAO,yBAChB,CACJ,EAEa,EAAb,cAAkD,KAAM,CACpD,aAAc,CACV,MAAM,+CAA+C,EACrD,KAAK,KAAO,8BAChB,CACJ,EAWa,EAAb,cAA6C,KAAM,CAE/C,aAEA,oBAOA,YAAY,EAAoC,EAA8B,EAAgB,CAC1F,MAAM,mDAAoD,CAAE,OAAM,CAAC,EACnE,KAAK,KAAO,0BACZ,KAAK,aAAe,EACpB,KAAK,oBAAsB,CAC/B,CACJ,EAOa,EAAb,KAA2B,CACvB,OAEA,YAAY,EAA6B,CACrC,KAAK,OAAS,CAClB,CAGA,OAAO,aAAuB,CAC1B,OAAO,EAAA,gBAAgB,CAC3B,CAEA,MAAc,cAAmD,CAC7D,GAAI,CAAC,EAAA,gBAAgB,EAAG,MAAM,IAAI,EAClC,OAAO,KAAK,OAAO,gBACb,MAAM,KAAK,OAAO,gBAAgB,EAClC,MAAM,UAAU,cAAc,KACxC,CAGA,YAAqD,CAEjD,OADI,OAAO,aAAiB,IAAoB,cACzC,aAAa,UACxB,CAQA,MAAM,mBAAqD,CACvD,GAAI,OAAO,aAAiB,IAAa,MAAM,IAAI,EACnD,OAAO,aAAa,kBAAkB,CAC1C,CAGA,MAAM,iBAAoD,CAEtD,OAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB,CACpD,CAGA,MAAM,cAAiC,CAGnC,OAFK,EAAA,gBAAgB,EAEd,MADgB,KAAK,gBAAgB,IACxB,KAFW,EAGnC,CAWA,MAAM,WAAuC,CAEzC,GAAI,MADqB,KAAK,kBAAkB,IAC7B,UAAW,MAAM,IAAI,EAExC,IAAM,EAAe,MAAM,KAAK,aAAa,EACvC,EAAW,MAAM,EAAa,YAAY,gBAAgB,EAChE,GAAI,EAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,EACxC,EAGX,IAAM,EAAe,MAAM,EAAa,YAAY,UAAU,CAC1D,gBAAiB,GACjB,qBAAsB,EAAA,sBAAsB,KAAK,OAAO,cAAc,CAC1E,CAAC,EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,EAC5C,CACX,CA2BA,MAAM,aAAgC,CAClC,IAAM,EAAe,MAAM,KAAK,gBAAgB,EAChD,GAAI,CAAC,EAAc,MAAO,GAC1B,IAAM,EAAO,EAAa,OAAO,EAC7B,EAA0C,KAC9C,GAAI,KAAK,OAAO,cACZ,GAAI,CACA,MAAM,KAAK,OAAO,cAAc,CAAI,CACxC,OAAS,EAAO,CACZ,EAAe,CAAE,OAAM,CAC3B,CAEJ,GAAI,CAAC,EAAc,OAAO,EAAa,YAAY,EACnD,IAAI,EACJ,GAAI,CACA,EAAW,MAAM,EAAa,YAAY,CAC9C,OAAS,EAAc,CACnB,MAAM,IAAI,EACN,EACA,GACI,eACA,CAAC,EAAa,MAAO,CAAY,EACjC,qDACJ,CACJ,CACJ,CACA,MAAM,IAAI,EAAwB,EAAM,EAAU,EAAa,KAAK,CACxE,CACJ"}
@@ -8,7 +8,13 @@ var n = class extends Error {
8
8
  constructor() {
9
9
  super("Permissão de notificação negada pelo usuário."), this.name = "WebPushPermissionDeniedError";
10
10
  }
11
- }, i = class {
11
+ }, i = class extends Error {
12
+ subscription;
13
+ browserUnsubscribed;
14
+ constructor(e, t, n) {
15
+ super("Falha ao remover a inscrição de push no backend.", { cause: n }), this.name = "WebPushUnsubscribeError", this.subscription = e, this.browserUnsubscribed = t;
16
+ }
17
+ }, a = class {
12
18
  config;
13
19
  constructor(e) {
14
20
  this.config = e;
@@ -45,10 +51,24 @@ var n = class extends Error {
45
51
  }
46
52
  async unsubscribe() {
47
53
  let e = await this.getSubscription();
48
- return e ? (this.config.onUnsubscribe && await this.config.onUnsubscribe(e.toJSON()), e.unsubscribe()) : !1;
54
+ if (!e) return !1;
55
+ let t = e.toJSON(), n = null;
56
+ if (this.config.onUnsubscribe) try {
57
+ await this.config.onUnsubscribe(t);
58
+ } catch (e) {
59
+ n = { cause: e };
60
+ }
61
+ if (!n) return e.unsubscribe();
62
+ let r;
63
+ try {
64
+ r = await e.unsubscribe();
65
+ } catch (e) {
66
+ throw new i(t, !1, AggregateError([n.cause, e], "Backend delete and browser unsubscribe both failed."));
67
+ }
68
+ throw new i(t, r, n.cause);
49
69
  }
50
70
  };
51
71
  //#endregion
52
- export { i as WebPushClient, r as WebPushPermissionDeniedError, n as WebPushUnsupportedError };
72
+ export { a as WebPushClient, r as WebPushPermissionDeniedError, i as WebPushUnsubscribeError, n as WebPushUnsupportedError };
53
73
 
54
74
  //# sourceMappingURL=web-push-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-push-client.js","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /** Remove the subscription from the backend. Called before `subscription.unsubscribe()`. */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription. Calls `onUnsubscribe` first (so the\n * backend can purge the record) and then `subscription.unsubscribe()`.\n *\n * @returns True when an active subscription was removed, false otherwise.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n if (this.config.onUnsubscribe) {\n await this.config.onUnsubscribe(subscription.toJSON());\n }\n return subscription.unsubscribe();\n }\n}\n"],"mappings":";;AAoBA,IAAa,IAAb,cAA6C,MAAM;CAC/C,cAAc;EAEV,AADA,MAAM,yCAAyC,GAC/C,KAAK,OAAO;CAChB;AACJ,GAEa,IAAb,cAAkD,MAAM;CACpD,cAAc;EAEV,AADA,MAAM,+CAA+C,GACrD,KAAK,OAAO;CAChB;AACJ,GAOa,IAAb,MAA2B;CACvB;CAEA,YAAY,GAA6B;EACrC,KAAK,SAAS;CAClB;CAGA,OAAO,cAAuB;EAC1B,OAAO,EAAgB;CAC3B;CAEA,MAAc,eAAmD;EAC7D,IAAI,CAAC,EAAgB,GAAG,MAAM,IAAI,EAAwB;EAC1D,OAAO,KAAK,OAAO,kBACb,MAAM,KAAK,OAAO,gBAAgB,IAClC,MAAM,UAAU,cAAc;CACxC;CAGA,aAAqD;EAEjD,OADI,OAAO,eAAiB,MAAoB,gBACzC,aAAa;CACxB;CAQA,MAAM,oBAAqD;EACvD,IAAI,OAAO,eAAiB,KAAa,MAAM,IAAI,EAAwB;EAC3E,OAAO,aAAa,kBAAkB;CAC1C;CAGA,MAAM,kBAAoD;EAEtD,QAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB;CACpD;CAGA,MAAM,eAAiC;EAGnC,OAFK,EAAgB,IAEd,MADgB,KAAK,gBAAgB,MACxB,OAFW;CAGnC;CAWA,MAAM,YAAuC;EAEzC,IAAI,MADqB,KAAK,kBAAkB,MAC7B,WAAW,MAAM,IAAI,EAA6B;EAErE,IAAM,IAAe,MAAM,KAAK,aAAa,GACvC,IAAW,MAAM,EAAa,YAAY,gBAAgB;EAChE,IAAI,GAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,GACxC;EAGX,IAAM,IAAe,MAAM,EAAa,YAAY,UAAU;GAC1D,iBAAiB;GACjB,sBAAsB,EAAsB,KAAK,OAAO,cAAc;EAC1E,CAAC;EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,GAC5C;CACX;CAQA,MAAM,cAAgC;EAClC,IAAM,IAAe,MAAM,KAAK,gBAAgB;EAKhD,OAJK,KACD,KAAK,OAAO,iBACZ,MAAM,KAAK,OAAO,cAAc,EAAa,OAAO,CAAC,GAElD,EAAa,YAAY,KAJN;CAK9B;AACJ"}
1
+ {"version":3,"file":"web-push-client.js","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Remove the subscription from the backend. Called before\n * `subscription.unsubscribe()`, with the subscription's JSON — key the\n * delete by its `endpoint`. A rejection no longer keeps the browser\n * subscribed: see {@link WebPushClient.unsubscribe}.\n */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Thrown by {@link WebPushClient.unsubscribe} when `onUnsubscribe` failed. The\n * browser subscription was still released — `browserUnsubscribed` says whether\n * the browser confirmed it — so the only thing left behind is the backend row.\n *\n * Carries the `subscription` JSON so the app can retry the delete by\n * `endpoint` later; if it never does, the push service answers the next send\n * to that endpoint with `404`/`410`, which is the backend's cue to drop the row.\n */\nexport class WebPushUnsubscribeError extends Error {\n /** The subscription whose backend delete failed. */\n readonly subscription: PushSubscriptionJSON;\n /** Whether `PushSubscription.unsubscribe()` reported success. */\n readonly browserUnsubscribed: boolean;\n\n /**\n * @param subscription - The subscription whose backend delete failed.\n * @param browserUnsubscribed - What `PushSubscription.unsubscribe()` returned.\n * @param cause - What `onUnsubscribe` threw.\n */\n constructor(subscription: PushSubscriptionJSON, browserUnsubscribed: boolean, cause: unknown) {\n super(\"Falha ao remover a inscrição de push no backend.\", { cause });\n this.name = \"WebPushUnsubscribeError\";\n this.subscription = subscription;\n this.browserUnsubscribed = browserUnsubscribed;\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription: `onUnsubscribe` first, so the backend\n * can purge the record while the endpoint is known, then\n * `subscription.unsubscribe()` — **always**, even when `onUnsubscribe`\n * failed.\n *\n * Why the browser is released anyway: the two failure modes are not\n * symmetric. A backend row left behind costs one send to a dead endpoint,\n * which the push service answers with `404`/`410` and the backend deletes.\n * A browser subscription left behind keeps delivering this account's\n * notifications to whoever uses the browser next — and the backend call\n * fails precisely when that matters, at the end of a session, when the\n * token that authenticates it is already gone. Before 0.77.0 a rejected\n * `onUnsubscribe` aborted here and the browser stayed subscribed (#445).\n * The endpoint is not lost either way: it is read before both calls and\n * travels in the error.\n *\n * @returns True when an active subscription was removed, false when there\n * was none or the browser refused.\n * @throws {WebPushUnsubscribeError} When `onUnsubscribe` failed — thrown\n * after the browser subscription was released. When the browser call also\n * threw, `browserUnsubscribed` is `false` and `cause` is an\n * `AggregateError` holding both errors, backend first, so the endpoint the\n * error carries is never traded for a bare `DOMException`.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n const json = subscription.toJSON();\n let backendError: { cause: unknown } | null = null;\n if (this.config.onUnsubscribe) {\n try {\n await this.config.onUnsubscribe(json);\n } catch (cause) {\n backendError = { cause };\n }\n }\n if (!backendError) return subscription.unsubscribe();\n let released: boolean;\n try {\n released = await subscription.unsubscribe();\n } catch (browserError) {\n throw new WebPushUnsubscribeError(\n json,\n false,\n new AggregateError(\n [backendError.cause, browserError],\n \"Backend delete and browser unsubscribe both failed.\",\n ),\n );\n }\n throw new WebPushUnsubscribeError(json, released, backendError.cause);\n }\n}\n"],"mappings":";;AAyBA,IAAa,IAAb,cAA6C,MAAM;CAC/C,cAAc;EAEV,AADA,MAAM,yCAAyC,GAC/C,KAAK,OAAO;CAChB;AACJ,GAEa,IAAb,cAAkD,MAAM;CACpD,cAAc;EAEV,AADA,MAAM,+CAA+C,GACrD,KAAK,OAAO;CAChB;AACJ,GAWa,IAAb,cAA6C,MAAM;CAE/C;CAEA;CAOA,YAAY,GAAoC,GAA8B,GAAgB;EAI1F,AAHA,MAAM,oDAAoD,EAAE,SAAM,CAAC,GACnE,KAAK,OAAO,2BACZ,KAAK,eAAe,GACpB,KAAK,sBAAsB;CAC/B;AACJ,GAOa,IAAb,MAA2B;CACvB;CAEA,YAAY,GAA6B;EACrC,KAAK,SAAS;CAClB;CAGA,OAAO,cAAuB;EAC1B,OAAO,EAAgB;CAC3B;CAEA,MAAc,eAAmD;EAC7D,IAAI,CAAC,EAAgB,GAAG,MAAM,IAAI,EAAwB;EAC1D,OAAO,KAAK,OAAO,kBACb,MAAM,KAAK,OAAO,gBAAgB,IAClC,MAAM,UAAU,cAAc;CACxC;CAGA,aAAqD;EAEjD,OADI,OAAO,eAAiB,MAAoB,gBACzC,aAAa;CACxB;CAQA,MAAM,oBAAqD;EACvD,IAAI,OAAO,eAAiB,KAAa,MAAM,IAAI,EAAwB;EAC3E,OAAO,aAAa,kBAAkB;CAC1C;CAGA,MAAM,kBAAoD;EAEtD,QAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB;CACpD;CAGA,MAAM,eAAiC;EAGnC,OAFK,EAAgB,IAEd,MADgB,KAAK,gBAAgB,MACxB,OAFW;CAGnC;CAWA,MAAM,YAAuC;EAEzC,IAAI,MADqB,KAAK,kBAAkB,MAC7B,WAAW,MAAM,IAAI,EAA6B;EAErE,IAAM,IAAe,MAAM,KAAK,aAAa,GACvC,IAAW,MAAM,EAAa,YAAY,gBAAgB;EAChE,IAAI,GAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,GACxC;EAGX,IAAM,IAAe,MAAM,EAAa,YAAY,UAAU;GAC1D,iBAAiB;GACjB,sBAAsB,EAAsB,KAAK,OAAO,cAAc;EAC1E,CAAC;EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,GAC5C;CACX;CA2BA,MAAM,cAAgC;EAClC,IAAM,IAAe,MAAM,KAAK,gBAAgB;EAChD,IAAI,CAAC,GAAc,OAAO;EAC1B,IAAM,IAAO,EAAa,OAAO,GAC7B,IAA0C;EAC9C,IAAI,KAAK,OAAO,eACZ,IAAI;GACA,MAAM,KAAK,OAAO,cAAc,CAAI;EACxC,SAAS,GAAO;GACZ,IAAe,EAAE,SAAM;EAC3B;EAEJ,IAAI,CAAC,GAAc,OAAO,EAAa,YAAY;EACnD,IAAI;EACJ,IAAI;GACA,IAAW,MAAM,EAAa,YAAY;EAC9C,SAAS,GAAc;GACnB,MAAM,IAAI,EACN,GACA,IACI,eACA,CAAC,EAAa,OAAO,CAAY,GACjC,qDACJ,CACJ;EACJ;EACA,MAAM,IAAI,EAAwB,GAAM,GAAU,EAAa,KAAK;CACxE;AACJ"}