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
@@ -714,6 +714,15 @@ export declare interface ApiClientConfig {
714
714
  prefix?: string;
715
715
  /** Returns the current bearer token (or null/undefined). Called per request. */
716
716
  getToken?: () => string | null | undefined;
717
+ /**
718
+ * Returns a counter that changes whenever the session changes — a sign-out,
719
+ * a sign-in. Read when each attempt leaves and handed to `refresh` and
720
+ * `onUnauthorized` in {@link RequestAuthContext}, so they can tell a `401`
721
+ * from an ended session (refuse it) from one of the current session (handle
722
+ * it). The token alone cannot: a request sent with no token, or with a token
723
+ * rehydrated from storage, says nothing about which session sent it.
724
+ */
725
+ getSessionEpoch?: () => number;
717
726
  /**
718
727
  * Origins besides `baseURL`'s that may receive the `getToken` credential.
719
728
  *
@@ -743,8 +752,12 @@ export declare interface ApiClientConfig {
743
752
  * Called when a request ends up unauthorized — a 401 with no `refresh`
744
753
  * configured, a `refresh()` that threw, or a retry that came back 401 again
745
754
  * after a refresh that resolved. Use it to end the session.
755
+ *
756
+ * `context` describes the attempt that came back `401` — the replay's, when
757
+ * it was the replay. Throwing {@link SessionSupersededError} here rejects the
758
+ * request with it; any other error is logged and the `401` is thrown as usual.
746
759
  */
747
- onUnauthorized?: (response: Response) => void | Promise<void>;
760
+ onUnauthorized?: (response: Response, context: RequestAuthContext) => void | Promise<void>;
748
761
  /**
749
762
  * Where the client reports each request it finished. Off when absent — the
750
763
  * client writes to no console of its own.
@@ -768,8 +781,17 @@ export declare interface ApiClientConfig {
768
781
  /**
769
782
  * Optional refresh hook. When provided and the original request returns 401,
770
783
  * the client awaits `refresh()` then retries the request once.
784
+ *
785
+ * The hook receives a {@link RequestAuthContext} naming the token the rejected
786
+ * request carried. A hook that ignores it keeps working; one that reads it
787
+ * can tell a stale `401` — the token already rotated since the request left —
788
+ * from a fresh one, and skip a second refresh.
789
+ *
790
+ * Throwing {@link SessionSupersededError} rejects the request with that
791
+ * error **without** calling `onUnauthorized`: the session the refresh
792
+ * belonged to is gone, and the one in its place must not be ended for it.
771
793
  */
772
- refresh?: () => Promise<void>;
794
+ refresh?: (context: RequestAuthContext) => Promise<void>;
773
795
  /**
774
796
  * Retry failed requests with exponential backoff. Off by default, so the
775
797
  * client keeps failing fast unless you opt in.
@@ -1072,6 +1094,25 @@ export declare interface AppErrorQueueEntry {
1072
1094
  report: AppErrorReportSchema;
1073
1095
  /** How many times this exact report was raised while it waited. */
1074
1096
  count: number;
1097
+ /**
1098
+ * The token in force when the report was raised, as a one-way fingerprint
1099
+ * (`"t:<hex>"`) — never the token itself. `null` means there was none;
1100
+ * the field is missing on entries persisted before it existed (≤ 0.76.0).
1101
+ *
1102
+ * The bearer is resolved when the queue drains, not when the error happens,
1103
+ * so this is what keeps a report user A raised offline from leaving with
1104
+ * user B's token: the bearer only goes out when it is the very token whose
1105
+ * fingerprint is stored here.
1106
+ */
1107
+ session?: string | null;
1108
+ /**
1109
+ * Who `getPrincipal` said was signed in when the report was raised, as a
1110
+ * one-way fingerprint (`"p:<hex>"`). Only set when the reporter has a
1111
+ * `getPrincipal`; `null` means it named nobody. It never grants the bearer
1112
+ * — `session` does — it only decides whether `foreignReports: "drop"`
1113
+ * discards the entry.
1114
+ */
1115
+ owner?: string | null;
1075
1116
  }
1076
1117
 
1077
1118
  /**
@@ -1090,9 +1131,22 @@ export declare interface AppErrorReporter {
1090
1131
  report: (error: unknown, context?: Record<string, unknown>) => void;
1091
1132
  /** Drain the queue now. Concurrent calls share one run. */
1092
1133
  flush: () => Promise<AppErrorFlushResult>;
1093
- /** The waiting entries, oldest first. */
1134
+ /**
1135
+ * The waiting entries, oldest first. With an async `getToken` and no
1136
+ * `getPrincipal`, a report joins them once its token resolves — `flush()`
1137
+ * waits for that, `pending()` does not.
1138
+ */
1094
1139
  pending: () => AppErrorQueueEntry[];
1095
- /** Drop every waiting report. */
1140
+ /**
1141
+ * Drop every waiting report. Call it on logout when reports raised in that
1142
+ * session should not outlive it; even without it, a report never leaves
1143
+ * with the next user's bearer.
1144
+ *
1145
+ * It also stops a flush in flight before its next send (the one already on
1146
+ * the wire cannot be recalled), and discards reports still waiting for an
1147
+ * async `getToken` to stamp them — both would otherwise refill the queue or
1148
+ * keep sending what the logout meant to forget.
1149
+ */
1096
1150
  clear: () => void;
1097
1151
  /** Remove the `online` listener and the retry timer. */
1098
1152
  dispose: () => void;
@@ -1740,6 +1794,70 @@ export declare type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
1740
1794
 
1741
1795
  export declare type AvatarStatus = "online" | "offline" | "busy";
1742
1796
 
1797
+ /**
1798
+ * `type` of the message the worker posts to window clients to ask about a queued
1799
+ * request. Answered by {@link provideBackgroundSyncCredentials}.
1800
+ */
1801
+ export declare const BACKGROUND_SYNC_CREDENTIALS_MESSAGE = "tempest:bg-sync-credentials";
1802
+
1803
+ /**
1804
+ * Resolve the credentials for a queued request: a header record to apply, or
1805
+ * `null` when there is no session (the entry is then dropped).
1806
+ */
1807
+ export declare type BackgroundSyncCredentialsProvider = (request: BackgroundSyncCredentialsRequest) => Record<string, string> | null | Promise<Record<string, string> | null>;
1808
+
1809
+ /** The request a window client is asked about, as sent by the worker. */
1810
+ export declare interface BackgroundSyncCredentialsRequest {
1811
+ /** Absolute URL of the queued request. */
1812
+ url: string;
1813
+ /** HTTP method of the queued request. */
1814
+ method: string;
1815
+ /**
1816
+ * `"enqueue"` when the worker only wants the owner of a request that just
1817
+ * failed; `"replay"` when it is about to send it and needs the credentials.
1818
+ */
1819
+ phase: "enqueue" | "replay";
1820
+ }
1821
+
1822
+ /**
1823
+ * How the default `beforeReplay` treats an `"unknown"` owner on an entry that
1824
+ * carried no credential header:
1825
+ * - `"strict"` (default): it asks the open pages like any other entry — replayed
1826
+ * only when every answer lacks a principal, dropped as soon as one answer has
1827
+ * one, pending while nobody answers.
1828
+ * - `"lenient"`: it is replayed as-is, without asking (the behavior up to 0.77.0).
1829
+ *
1830
+ * An entry with stripped credentials always asks, under either policy.
1831
+ */
1832
+ export declare type BackgroundSyncOwnerPolicy = "strict" | "lenient";
1833
+
1834
+ /**
1835
+ * How an entry is tied to whoever queued it:
1836
+ * - `"bound"`: the issuing page reported a principal (or `null` for nobody);
1837
+ * the entry only goes out while the same one is signed in.
1838
+ * - `"unbound"`: the issuing page answered without `getPrincipal`.
1839
+ * - `"unknown"`: nobody could say — no issuing tab, no answer, an error, or an
1840
+ * entry written before owners existed. Fails closed, with or without stripped
1841
+ * credentials: a cookie session rides along on the replay, so an entry with no
1842
+ * credential header is checked like any other.
1843
+ *
1844
+ * While the worker is still asking, the stored state is `"pending"`, which a
1845
+ * replay never sees: it waits, then reads a stamp that never came as `"unknown"`.
1846
+ */
1847
+ export declare type BackgroundSyncOwnerState = "bound" | "unbound" | "unknown";
1848
+
1849
+ /** What a queued request looked like when it was stored, passed to `beforeReplay`. */
1850
+ export declare interface BackgroundSyncReplayContext {
1851
+ /** Epoch milliseconds of the original failed attempt. */
1852
+ queuedAt: number;
1853
+ /** Lower-cased credential header names stripped before the entry was stored. */
1854
+ strippedHeaders: readonly string[];
1855
+ /** How the entry is tied to its owner. */
1856
+ ownerState: BackgroundSyncOwnerState;
1857
+ /** Owner fingerprint under `"bound"` (`null` for nobody); `undefined` otherwise. */
1858
+ owner: string | null | undefined;
1859
+ }
1860
+
1743
1861
  /**
1744
1862
  * Pill / square status badge.
1745
1863
  *
@@ -2468,9 +2586,10 @@ export declare type CacheFilter = string | RegExp | ((name: string) => boolean);
2468
2586
 
2469
2587
  /**
2470
2588
  * Main-thread helpers to inspect and clear the Cache Storage buckets a service
2471
- * worker fills — for a "X MB cacheado" readout and a "limpar cache" action
2472
- * (e.g. on logout). All guard `caches` so they no-op safely under SSR / older
2473
- * browsers.
2589
+ * worker fills — for a "X MB cacheado" readout and a "limpar cache" action.
2590
+ * On logout prefer `clearSessionData`, which also drops the background-sync
2591
+ * queue and spares the app-shell precache. All guard `caches` so they no-op
2592
+ * safely under SSR / older browsers.
2474
2593
  */
2475
2594
  /** Per-cache usage summary returned by {@link inspectCaches}. */
2476
2595
  export declare interface CacheReport {
@@ -3109,6 +3228,28 @@ declare type ClassValue = string | number | bigint | boolean | null | undefined
3109
3228
  */
3110
3229
  export declare function clearAnnouncer(): void;
3111
3230
 
3231
+ /**
3232
+ * Empty the background-sync queue — every request still waiting to be replayed —
3233
+ * and bump its clear epoch. Call it on logout (or use `clearSessionData()`, which
3234
+ * also clears the runtime caches) so mutations made under one session are never
3235
+ * replayed under the next.
3236
+ *
3237
+ * Works from the page and from the worker, and reaches work already in flight
3238
+ * in the worker: a replay checks each entry right before sending it, and a
3239
+ * request still failing when the queue is cleared is not written afterwards.
3240
+ *
3241
+ * @param queueName - The `queueName` given to {@link installBackgroundSync}.
3242
+ * Default `tempest-bg-sync`.
3243
+ * @returns `true` once the queue is empty; `false` when IndexedDB is
3244
+ * unavailable (SSR, old browsers).
3245
+ *
3246
+ * @example
3247
+ * import { clearBackgroundSyncQueue } from "tempest-react-sdk/sw";
3248
+ *
3249
+ * await clearBackgroundSyncQueue();
3250
+ */
3251
+ export declare function clearBackgroundSyncQueue(queueName?: string): Promise<boolean>;
3252
+
3112
3253
  /**
3113
3254
  * Delete Cache Storage buckets whose name passes `filter` (all of them when no
3114
3255
  * filter is given).
@@ -3117,10 +3258,60 @@ export declare function clearAnnouncer(): void;
3117
3258
  * @returns The names of the caches that were deleted.
3118
3259
  *
3119
3260
  * @example
3120
- * await clearCaches("tempest-"); // drop every SDK-managed cache on logout
3261
+ * await clearCaches("tempest-api"); // drop one runtime bucket
3121
3262
  */
3122
3263
  export declare function clearCaches(filter?: CacheFilter): Promise<string[]>;
3123
3264
 
3265
+ /**
3266
+ * Remove everything the service-worker helpers keep on behalf of the signed-in
3267
+ * user: the runtime caches filled by `installRuntimeCache` and the queue of
3268
+ * mutations waiting in `installBackgroundSync`. Call it on logout, from the page.
3269
+ *
3270
+ * Both stores outlive the session and are shared by every account that signs in
3271
+ * on the browser. Measured in Chromium 153 (11/10/2026), before this existed:
3272
+ * the documented `clearCaches("tempest-")` deleted nothing because the recipe's
3273
+ * runtime cache was named `api`, the next account read the previous account's
3274
+ * `GET /api/me` offline, and a mutation queued by the previous account was
3275
+ * replayed with its stored bearer after logout. A prefix filter cannot know the
3276
+ * names a recipe chose, so the default here inverts it: every bucket goes except
3277
+ * the precache, which holds only the build's static assets.
3278
+ *
3279
+ * Cache Storage and IndexedDB are origin storage, readable from the window, so
3280
+ * this needs no message to the worker.
3281
+ *
3282
+ * @param options - Narrow the caches to delete, or name a custom queue.
3283
+ * @returns The caches deleted and whether the queue was cleared.
3284
+ *
3285
+ * @example
3286
+ * import { clearSessionData } from "tempest-react-sdk/sw";
3287
+ *
3288
+ * async function logout() {
3289
+ * await clearSessionData();
3290
+ * }
3291
+ */
3292
+ export declare function clearSessionData(options?: ClearSessionDataOptions): Promise<ClearSessionDataResult>;
3293
+
3294
+ /** Options for {@link clearSessionData}. */
3295
+ export declare interface ClearSessionDataOptions {
3296
+ /**
3297
+ * Which Cache Storage buckets hold session data. Default: every bucket except
3298
+ * the app-shell precache (`tempest-precache-*`), so the app still launches
3299
+ * offline after logout while no runtime entry — whatever its `cacheName` —
3300
+ * survives into the next session.
3301
+ */
3302
+ caches?: CacheFilter;
3303
+ /** The background-sync `queueName` to clear. Default `tempest-bg-sync`. */
3304
+ queueName?: string;
3305
+ }
3306
+
3307
+ /** What {@link clearSessionData} removed. */
3308
+ export declare interface ClearSessionDataResult {
3309
+ /** Names of the Cache Storage buckets deleted. */
3310
+ caches: string[];
3311
+ /** Whether the background-sync queue was emptied. */
3312
+ queueCleared: boolean;
3313
+ }
3314
+
3124
3315
  /**
3125
3316
  * Wraps its children in a `<div>` and invokes `onOutside` whenever a
3126
3317
  * `mousedown` or `touchstart` event occurs outside that subtree.
@@ -3670,6 +3861,12 @@ export declare function createApiClient(config: ApiClientConfig): ApiClient;
3670
3861
  * - **any other 4xx** — dropped: the backend will refuse that body forever, and
3671
3862
  * keeping it would block every report queued behind it.
3672
3863
  *
3864
+ * Each report is stamped with a one-way fingerprint of the token in force when
3865
+ * it was raised, and goes out with a bearer only when the token at send time is
3866
+ * that same one — never with the token of whoever signed in next. The queue
3867
+ * never stores a token. `clear()` also stops a flush in flight and discards
3868
+ * reports still waiting for an async token.
3869
+ *
3673
3870
  * @example
3674
3871
  * import { createAppErrorReporter } from "tempest-react-sdk";
3675
3872
  *
@@ -3696,6 +3893,44 @@ export declare interface CreateAppErrorReporterOptions {
3696
3893
  * to get through. A token that throws or resolves empty sends anonymously.
3697
3894
  */
3698
3895
  getToken?: () => string | null | undefined | Promise<string | null | undefined>;
3896
+ /**
3897
+ * Who is signed in right now — a stable, non-secret id such as the user id.
3898
+ * Read **when the error is reported**, and stored in the queue only as a
3899
+ * one-way fingerprint (`entry.owner`). A non-string (an `async` getter
3900
+ * from untyped JS, an object) or a getter that throws reads as nobody.
3901
+ *
3902
+ * It never grants the bearer. The bearer goes out only when the token at
3903
+ * send time is the very token that was in force at report time (its
3904
+ * fingerprint is `entry.session`): the principal and the token come from
3905
+ * two sources, and a principal store that lags behind a login would
3906
+ * otherwise send A's report with B's token. The principal only tells
3907
+ * {@link CreateAppErrorReporterOptions.foreignReports} `"drop"` whose
3908
+ * report is whose.
3909
+ */
3910
+ getPrincipal?: () => string | number | null | undefined;
3911
+ /**
3912
+ * What to do with a queued report that cannot go out with the current
3913
+ * bearer — raised under another token, or queued before 0.77.0. Default
3914
+ * `"anonymous"`.
3915
+ *
3916
+ * - `"anonymous"` — send it without `Authorization`. The route is public,
3917
+ * so the error still reaches the backend; it is only not attributed to
3918
+ * anybody. This includes the user's own reports raised before a token
3919
+ * refresh: attribution is lost, never misplaced.
3920
+ * - `"drop"` — discard every report not proven to be the current
3921
+ * principal's: another principal, nobody, or unknown (no principal at
3922
+ * report time, a legacy entry, or nobody signed in now). For apps where
3923
+ * a previous user's error text must not leave the device after they
3924
+ * signed out. **Needs `getPrincipal`**: without it the only identity is
3925
+ * the token fingerprint, and a refresh changes it, so a mismatch is no
3926
+ * proof the report is someone else's — the reporter then sends
3927
+ * anonymously instead of destroying the user's own reports.
3928
+ *
3929
+ * Never is a report sent with a bearer other than the one it was raised
3930
+ * under: that would credit the previous user's error to whoever signed in
3931
+ * next.
3932
+ */
3933
+ foreignReports?: "anonymous" | "drop";
3699
3934
  /**
3700
3935
  * Device facts, read **when the error is reported**, not when it is sent —
3701
3936
  * a report queued offline on `1.4.0` must not go out labelled `1.5.0`.
@@ -3870,6 +4105,10 @@ export declare function createColorScale(hex: string, mode?: "light" | "dark", o
3870
4105
  * `GET /{resource}/{id}`, create → `POST`, update → `PATCH`/`PUT`,
3871
4106
  * delete → `DELETE`.
3872
4107
  *
4108
+ * Every id goes through {@link encodeIdSegment} before reaching `buildPath`, so
4109
+ * an id can never leave its own path segment — not with the default builder,
4110
+ * and not with an override that interpolates it.
4111
+ *
3873
4112
  * @param client - The HTTP client created by `createApiClient`.
3874
4113
  * @param options - Optional overrides for param names, sort encoding,
3875
4114
  * update method and path building.
@@ -4224,10 +4463,14 @@ export declare function createLinkStatsSampler(options?: LinkStatsSamplerOptions
4224
4463
  * file. Apps that already have `createOfflineStore` open can pass their own
4225
4464
  * {@link ResumableUploadStorage} instead.
4226
4465
  *
4466
+ * `clear()` forgets every record under the prefix. Call it from
4467
+ * `onSessionEnd`: a record outlives the session that wrote it, and only
4468
+ * completing the upload or `abort({ discard: true })` removes it otherwise.
4469
+ *
4227
4470
  * @param prefix - Key prefix. Default `"tempest-upload:"`.
4228
4471
  * @returns A storage that no-ops when `localStorage` is unavailable.
4229
4472
  */
4230
- export declare function createLocalUploadStorage(prefix?: string): ResumableUploadStorage;
4473
+ export declare function createLocalUploadStorage(prefix?: string): LocalUploadStorage;
4231
4474
 
4232
4475
  /**
4233
4476
  * Create a structured leveled logger. Plug arbitrary sinks (Sentry, Datadog,
@@ -4415,7 +4658,7 @@ export declare function createOSRMBackend(options: OSRMBackendOptions): RoutingB
4415
4658
  /**
4416
4659
  * Build a `206 Partial Content` response from a full one for an HTTP `Range`
4417
4660
  * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix
4418
- * `bytes=-suffixLength`. Returns the original response when there is no usable
4661
+ * `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable
4419
4662
  * `Range` header, or a `416` when the range is unsatisfiable.
4420
4663
  *
4421
4664
  * @param request The incoming request (its `Range` header drives the slice).
@@ -4607,15 +4850,26 @@ export declare function createQueryKeys<TKey extends string, TEntries extends Re
4607
4850
  * is already fresh. Measured against a mock backend, five concurrent expired
4608
4851
  * requests took two refreshes, not one.
4609
4852
  *
4610
- * Pass `getToken` to close that gap. The queue remembers the token its last
4611
- * refresh produced, and a call that finds that same token still in place returns
4612
- * immediately, because the refresh it was about to perform has already happened.
4853
+ * Pass `getToken` to close that gap. Wired as `createApiClient`'s `refresh`, the
4854
+ * queue is told which token each rejected request carried: when that token is
4855
+ * no longer the current one, the rotation it was about to trigger already
4856
+ * happened, so the call returns at once and the replay picks up the new token.
4613
4857
  * The same five requests then take exactly one refresh, and so do twenty.
4614
4858
  *
4859
+ * Called by hand, with no context, the queue falls back to remembering the
4860
+ * token its last refresh produced and skipping while that token is in place.
4861
+ * That fallback cannot tell "already refreshed" from "the refreshed token has
4862
+ * since expired" — both leave the same string in the store — which is why the
4863
+ * client passes the context. Before 0.77.0 the client did not, and every
4864
+ * access token a refresh issued became unrefreshable once it expired: its `401`
4865
+ * skipped the refresh, the replay failed again, and `onUnauthorized` ended the
4866
+ * session.
4867
+ *
4615
4868
  * @param refresh - Performs the refresh and installs the new credentials.
4616
4869
  * @param options - Optional token reader that enables already-refreshed
4617
4870
  * detection.
4618
- * @returns A function that refreshes at most once per rotation.
4871
+ * @returns A function that refreshes at most once per rotation, accepting the
4872
+ * {@link RequestAuthContext} `createApiClient` passes on a `401`.
4619
4873
  *
4620
4874
  * @example
4621
4875
  * const refresh = createRefreshQueue(() => AuthService.refresh(), {
@@ -4626,7 +4880,7 @@ export declare function createQueryKeys<TKey extends string, TEntries extends Re
4626
4880
  * await refresh();
4627
4881
  * // ...retry the original request
4628
4882
  */
4629
- export declare function createRefreshQueue(refresh: () => Promise<void>, options?: CreateRefreshQueueOptions): () => Promise<void>;
4883
+ export declare function createRefreshQueue(refresh: () => Promise<void>, options?: CreateRefreshQueueOptions): (context?: Pick<RequestAuthContext, "token">) => Promise<void>;
4630
4884
 
4631
4885
  export declare interface CreateRefreshQueueOptions {
4632
4886
  /**
@@ -4842,6 +5096,30 @@ export declare interface CreateStorePersistOptions<T> {
4842
5096
  * `redirectTo` set, the browser also leaves the page; without it, clearing the
4843
5097
  * store is enough for a `<RouteGuard>` to navigate on its own.
4844
5098
  *
5099
+ * Every refresh is bound to a **session epoch**, bumped by `logout()` and by a
5100
+ * `login()` whose credentials were accepted. A refresh that resolves after its epoch ended discards
5101
+ * what it brought back and rejects with {@link SessionSupersededError}, which
5102
+ * the client passes through without ending the new session. Before 0.77.0 a
5103
+ * refresh that was in flight during `logout()` reinstalled the old tokens —
5104
+ * and after `logout()` + `login(B)`, replaced B's token with A's while the UI
5105
+ * showed B (#435). A `401` from a request sent under an ended session is
5106
+ * refused the same way, instead of being replayed as the account signed in now.
5107
+ *
5108
+ * A `401` from a request that carried no token, while no session is installed,
5109
+ * surfaces without a refresh: in cookie mode that refresh would be answered by
5110
+ * the HttpOnly cookie `logout()` cannot clear, signing the previous account
5111
+ * back in (#474). With a `login()` in flight, that `401` waits for it and
5112
+ * replays with the token it installed, or surfaces when it failed — still
5113
+ * without a refresh (#496). Restore a session at boot with an explicit
5114
+ * `refresh()`, and pass `logoutPath` so the backend revokes the cookie:
5115
+ * without a working revocation, that boot `refresh()` restores the previous
5116
+ * account for whoever uses the browser next.
5117
+ *
5118
+ * Refreshes are collapsed per rotation, not just per overlap: a `401` whose
5119
+ * request left with a token that has since been replaced in the same session
5120
+ * replays with the new token and refreshes nothing. Three expired requests take
5121
+ * one refresh whether their `401`s arrive together or one after another.
5122
+ *
4845
5123
  * @example
4846
5124
  * const auth = createTempestAuth<User, { email: string; password: string }>({
4847
5125
  * baseURL: import.meta.env.VITE_API_URL,
@@ -4850,7 +5128,7 @@ export declare interface CreateStorePersistOptions<T> {
4850
5128
  *
4851
5129
  * await auth.login({ email, password }); // stores session, returns the user
4852
5130
  * const orders = await auth.api.get("/api/orders"); // sends the bearer token
4853
- * auth.logout();
5131
+ * await auth.logout(); // clears, then runs onSessionEnd
4854
5132
  *
4855
5133
  * @param options - The auth configuration.
4856
5134
  * @returns The store hook, a wired API client, and login/logout/refresh helpers.
@@ -4877,6 +5155,51 @@ export declare interface CreateTempestAuthOptions<TUser> {
4877
5155
  loginPath?: string;
4878
5156
  /** Refresh route (`POST`). Default: `"/api/auth/refresh"`. */
4879
5157
  refreshPath?: string;
5158
+ /**
5159
+ * Server-side logout route (`POST`), called by an explicit `logout()` to
5160
+ * revoke the ending session. Off by default.
5161
+ *
5162
+ * `logout()` alone only clears client state, and an HttpOnly refresh
5163
+ * cookie is out of its reach: with `withCredentials`, the cookie stays
5164
+ * redeemable until the backend revokes it. The tempest-fastapi-sdk mounts
5165
+ * `POST /auth/logout` (`204`, idempotent) when it stores refresh tokens in
5166
+ * the database — at `/auth/cookie/logout` when it delivers tokens both in
5167
+ * the body and in cookies — and stateless refresh tokens cannot be
5168
+ * revoked, so the route is absent there.
5169
+ *
5170
+ * The request goes out through the ended session's client (the same one
5171
+ * `onSessionEnd` receives, carrying its access token), with the body that
5172
+ * `refreshBody` builds from the refresh token stored before the clear:
5173
+ * `{ refresh_token }` in body mode, no body in cookie mode, which is what
5174
+ * that route reads. The local clear does not wait on it — `isAuthenticated`
5175
+ * is still `false` when `logout()` returns — but the promise `logout()`
5176
+ * returns does, so a navigation after `await auth.logout()` does not abort
5177
+ * it. A failure (network, `4xx`, `5xx`) is swallowed: revocation is
5178
+ * best-effort and never keeps the user signed in. A `login()` started
5179
+ * while it is in flight waits for it, because the logout response clears
5180
+ * the auth cookies and would otherwise clear the new account's.
5181
+ *
5182
+ * Only an explicit `logout()` revokes. On `"switch"` the cookie jar already
5183
+ * holds the new account's cookie, and on `"expired"` the server has already
5184
+ * refused the session.
5185
+ *
5186
+ * Overlapping sign-outs each send their own revocation, and `login()` waits
5187
+ * for all of them — at most 10 seconds, so one that
5188
+ * never answers delays the sign-in instead of blocking it. In cookie mode,
5189
+ * a revocation also waits (within the same bound) for the login and
5190
+ * refresh responses that were in flight when `logout()` ran: the server
5191
+ * sets the ended session's cookie when that response arrives, and a
5192
+ * revocation sent earlier would leave it behind. In body mode, the refresh
5193
+ * token such a superseded response brings back is revoked on its own.
5194
+ */
5195
+ logoutPath?: string;
5196
+ /**
5197
+ * Called with the error when a `logoutPath` revocation fails — network
5198
+ * failure, `4xx` or `5xx`. `logout()` still resolves, since the local
5199
+ * sign-out already happened; this is where the app logs or retries. A dev
5200
+ * build also warns in the console. Errors it throws are swallowed.
5201
+ */
5202
+ onLogoutError?: (error: unknown) => void;
4880
5203
  /** Optional current-user route (`GET`) called after login/refresh. */
4881
5204
  mePath?: string;
4882
5205
  /** Persist key for the store. Default: `"tempest-auth"`. */
@@ -4929,6 +5252,48 @@ export declare interface CreateTempestAuthOptions<TUser> {
4929
5252
  * outside any guarded subtree and a full reload is acceptable.
4930
5253
  */
4931
5254
  redirectTo?: string;
5255
+ /**
5256
+ * Tear down per-user state when a session ends — on an explicit `logout()`,
5257
+ * when the client ends the session on its own after a failed refresh, and
5258
+ * when a successful `login()` replaces a live session. Without it, the automatic path had no place to hook into, and
5259
+ * state derived from the user (a push subscription, a persisted query
5260
+ * cache, a background-sync queue) survived into the next account.
5261
+ *
5262
+ * Order, on every path:
5263
+ *
5264
+ * 1. any refresh still in flight is superseded, so it cannot reinstall the
5265
+ * ending session's tokens;
5266
+ * 2. the store and the stored refresh token are cleared — synchronously, so
5267
+ * `isAuthenticated` is already `false` when `logout()` returns;
5268
+ * 3. this hook runs with a {@link TempestSessionEnd} whose `api` still sends
5269
+ * the ended session's access token;
5270
+ * 4. on `"expired"` with `redirectTo` set, the browser navigates — after the
5271
+ * hook settled, even if it threw; on `"switch"`, the new session is
5272
+ * installed — after the hook settled, even if it threw.
5273
+ *
5274
+ * It may be async. `logout()` resolves when it settles and rejects with what
5275
+ * it threw; on the automatic path the error is swallowed, because the
5276
+ * request that ended the session already rejects with its own `401`. It
5277
+ * runs once per session: a second `logout()`, or the other requests of a
5278
+ * burst of `401`s, find nothing left to end and skip it.
5279
+ *
5280
+ * On `"switch"` the hook runs only **after** the new credentials were
5281
+ * accepted, so a failed `login()` leaves the live session untouched. It runs
5282
+ * **before** the new session is installed: teardown of the previous
5283
+ * account's state (clearing a persisted cache, a background-sync queue) has
5284
+ * to finish before the new account's requests start filling them, or it
5285
+ * would wipe — or race with — the new account's data. The cost is that
5286
+ * `isAuthenticated` is `false` while the hook runs, which a route guard may
5287
+ * act on; keep the hook short. Its error is swallowed there, since the
5288
+ * login itself succeeded.
5289
+ *
5290
+ * Inside the hook, reach the backend only through `session.api`. `auth.api`
5291
+ * goes out with no token; `auth.login()` waits on the teardown in progress —
5292
+ * this hook — and never settles. Telling the hook apart from an outside
5293
+ * caller would need an async context the browser does not have, so this is
5294
+ * a documented rule, not a runtime check.
5295
+ */
5296
+ onSessionEnd?: (session: TempestSessionEnd<TUser>) => void | Promise<void>;
4932
5297
  }
4933
5298
 
4934
5299
  /**
@@ -5403,6 +5768,33 @@ export declare interface CsvOptions {
5403
5768
  delimiter?: "," | ";";
5404
5769
  /** Prefix the output with a UTF-8 BOM. Default `true`. */
5405
5770
  bom?: boolean;
5771
+ /**
5772
+ * Prefix `'` to every text field that a spreadsheet would read as a formula.
5773
+ * Default `true`.
5774
+ *
5775
+ * A field starting with `=`, `+`, `-`, `@`, TAB or CR is a formula start by the
5776
+ * OWASP list (Excel and Google Sheets read it so; LibreOffice 26.2 evaluated only
5777
+ * `=`), and opening the file evaluates it: a name typed
5778
+ * as `=HYPERLINK("https://evil/?d="&A1,"ver")` turns into a link that exfiltrates
5779
+ * other cells (OWASP "CSV Injection"). Measured on LibreOffice 26.2: `=1+2` was
5780
+ * imported as the formula `of:=1+2` with value `3`. The default is the safe one
5781
+ * because the BOM and the `;` delimiter already aim the file at a spreadsheet,
5782
+ * and because the app that exports rarely controls every value in it.
5783
+ *
5784
+ * Text holding a `,`, `;` or TAB is also quoted whatever the delimiter
5785
+ * (#481): a spreadsheet whose list separator is not the file's would otherwise
5786
+ * split the bare field there and could start a cell with `=`. A quoted field is
5787
+ * not split, so the quotes close that path without touching the text —
5788
+ * `x;=1+2` is written `"x;=1+2"`.
5789
+ *
5790
+ * Numbers are never prefixed: a `number` cell, or a text field that is only a
5791
+ * number (`"-5"`, `"-1.234,56"`, `"+42"`, `"1e-7"`), cannot be a formula, and
5792
+ * neither can a lone `"-"`, which is left alone too. Pass
5793
+ * `false` when the file feeds another system's importer rather than a person's
5794
+ * spreadsheet, so the bytes round-trip unchanged and only the delimiter, quotes
5795
+ * and line breaks decide the quoting.
5796
+ */
5797
+ neutralizeFormulas?: boolean;
5406
5798
  }
5407
5799
 
5408
5800
  /** Cursor-paginated response envelope (`CursorPaginationSchema[T]`). */
@@ -5530,6 +5922,11 @@ export declare interface DataProviderOptions {
5530
5922
  /**
5531
5923
  * Build the request path for a resource (and optional id).
5532
5924
  * Default: `id == null ? "/" + resource : "/" + resource + "/" + id`.
5925
+ *
5926
+ * Since 0.77.0 `id` arrives already passed through {@link encodeIdSegment}:
5927
+ * percent-encoded as one path segment, and never `""`, `"."` or `".."`. An
5928
+ * override therefore interpolates it as-is — encoding it again would turn
5929
+ * `%2F` into `%252F`.
5533
5930
  */
5534
5931
  buildPath?: (resource: string, id?: string | number) => string;
5535
5932
  }
@@ -6013,6 +6410,19 @@ export declare const DEFAULT_CHUNK_SIZE: number;
6013
6410
  */
6014
6411
  export declare const DEFAULT_CIRCUITY_FACTOR = 1.3;
6015
6412
 
6413
+ /**
6414
+ * Header names treated as credentials by the service-worker helpers: never
6415
+ * persisted by `installBackgroundSync`, and a request carrying any of them
6416
+ * bypasses `installRuntimeCache` unless the route opts in. `Cookie` and
6417
+ * `Proxy-Authorization` are listed for completeness although the browser
6418
+ * forbids scripts from setting them — a cookie session is invisible to the
6419
+ * worker. The runtime cache therefore depends on `Cache-Control` and
6420
+ * `clearSessionData()`; the background-sync queue checks the entry's owner
6421
+ * through `getPrincipal` whether or not a credential header was stripped, and
6422
+ * `clearSessionData()` still backs that check up.
6423
+ */
6424
+ export declare const DEFAULT_CREDENTIAL_HEADERS: readonly string[];
6425
+
6016
6426
  /**
6017
6427
  * Default cookie the CSRF token is read from.
6018
6428
  *
@@ -6414,7 +6824,8 @@ export declare type DocumentVisibility = "visible" | "hidden";
6414
6824
  * @param rows - The rows to export.
6415
6825
  * @param columns - Columns, in the order they should appear.
6416
6826
  * @param fileName - File name offered to the user. Default `"export.csv"`.
6417
- * @param options - Delimiter and BOM, forwarded to {@link toCsv}.
6827
+ * @param options - Delimiter, BOM and formula neutralization, forwarded to
6828
+ * {@link toCsv}.
6418
6829
  * @returns A promise that resolves once the share or download completes.
6419
6830
  */
6420
6831
  export declare function downloadCsv<T>(rows: readonly T[], columns: readonly CsvColumn<T>[], fileName?: string, options?: CsvOptions): Promise<void>;
@@ -6603,6 +7014,34 @@ export declare interface EmptyStateProps {
6603
7014
  className?: string;
6604
7015
  }
6605
7016
 
7017
+ /**
7018
+ * Encode a record id as exactly one URL path segment.
7019
+ *
7020
+ * The data provider calls this on every id before building a path, because the
7021
+ * id is the one part of the URL that may come from outside the app — a search
7022
+ * param, a hash, a decoded route param, a value another user stored. Left raw,
7023
+ * `"../../admin/users"` walks the authenticated request out of the resource,
7024
+ * `"7?role=admin"` injects query params and `"7#x"` truncates the path, all
7025
+ * while the request still carries the bearer token (#438).
7026
+ *
7027
+ * `encodeURIComponent` neutralises `/`, `?`, `#` and `%`. It leaves `.` alone,
7028
+ * so `""`, `"."` and `".."` are rejected instead: the URL parser resolves them
7029
+ * as dot-segments whatever the encoding, and none of them names a record.
7030
+ * `null` and `undefined` — what an untyped caller or a not-yet-loaded route
7031
+ * param hands over — are rejected too, since `String()` would turn them into
7032
+ * the literal segments `"null"` and `"undefined"`.
7033
+ *
7034
+ * @param id - The record id, as handed to the provider.
7035
+ * @returns The percent-encoded segment, safe to interpolate between two `/`.
7036
+ * @throws {TypeError} When the id is `null`, `undefined`, `""`, `"."` or `".."`.
7037
+ *
7038
+ * @example
7039
+ * encodeIdSegment(7); // "7"
7040
+ * encodeIdSegment("a/b?x=1"); // "a%2Fb%3Fx%3D1"
7041
+ * encodeIdSegment(".."); // throws TypeError
7042
+ */
7043
+ export declare function encodeIdSegment(id: string | number): string;
7044
+
6606
7045
  /**
6607
7046
  * Encode text into a QR matrix.
6608
7047
  *
@@ -6784,6 +7223,53 @@ export declare interface EventStreamMessage<T> {
6784
7223
 
6785
7224
  export declare type EventStreamStatus = "idle" | "connecting" | "open" | "closed" | "error";
6786
7225
 
7226
+ /**
7227
+ * Finish a `tempest-fastapi-sdk` social login from the SPA's callback route.
7228
+ *
7229
+ * Reads the redirect with {@link readOAuthCallbackParams} and forwards `code`
7230
+ * **and** `state` to `GET {prefix}/oauth/{provider}/callback`, where the backend
7231
+ * compares the state against its `HttpOnly` cookie, redeems the code and
7232
+ * answers with the same JWT pair `POST {prefix}/login` returns.
7233
+ *
7234
+ * The request goes with `credentials: "include"` because the state cookie is
7235
+ * the other half of the comparison; with `skipAuth`, since there is no session
7236
+ * yet and a `401` here means a forged or expired callback, not an expired token
7237
+ * to refresh; and with `retry: false`, because the backend clears the state
7238
+ * cookie on the first attempt and a code redeems once.
7239
+ *
7240
+ * Pass it as the `exchange` of {@link useOAuthCallback}, which runs it once even
7241
+ * under StrictMode.
7242
+ *
7243
+ * @param api - The client from `createApiClient` (or `createTempestAuth().api`).
7244
+ * @param provider - The provider key registered on the backend (e.g. `"google"`).
7245
+ * @param options - Router prefix, the query string to read, abort signal.
7246
+ * @returns The backend's login response.
7247
+ * @throws {OAuthCallbackError} Before any request, when the redirect carries
7248
+ * `error` or lacks `state` or `code`.
7249
+ *
7250
+ * @example
7251
+ * const { loading, error } = useOAuthCallback<LoginResponse>({
7252
+ * exchange: () => exchangeOAuthCallback<LoginResponse>(api, "google"),
7253
+ * onSuccess: () => navigate("/", { replace: true }),
7254
+ * });
7255
+ */
7256
+ export declare function exchangeOAuthCallback<T>(api: ApiClient, provider: string, options?: ExchangeOAuthCallbackOptions): Promise<T>;
7257
+
7258
+ /** Options for {@link exchangeOAuthCallback}. */
7259
+ export declare interface ExchangeOAuthCallbackOptions {
7260
+ /**
7261
+ * Path the backend's auth router is mounted at — the `prefix` given to
7262
+ * `make_auth_router`. Default: `"/auth"`.
7263
+ */
7264
+ prefix?: string;
7265
+ /** Where to read the provider's redirect from. Default: `window.location.search`. */
7266
+ search?: string | URLSearchParams;
7267
+ /** Forwarded to {@link readOAuthCallbackParams}. Default: scrub when reading `window.location`. */
7268
+ scrubUrl?: boolean;
7269
+ /** Abort the exchange. */
7270
+ signal?: AbortSignal;
7271
+ }
7272
+
6787
7273
  /**
6788
7274
  * Grow a bounding box outward by `ratio` of its own span on every side, with a
6789
7275
  * small absolute floor so a single-point (zero-span) box still gets padding.
@@ -8217,9 +8703,17 @@ export declare function inspectCaches(options?: {
8217
8703
 
8218
8704
  /**
8219
8705
  * Install the background-sync queue: on a failed mutating request, the request
8220
- * is serialized to IndexedDB and a sync is registered; the original fetch still
8221
- * rejects (so your app can show an offline state), and the request is replayed
8222
- * later when the network returns.
8706
+ * is serialized to IndexedDB (without its credential headers) and a sync is
8707
+ * registered; the original fetch still rejects (so your app can show an offline
8708
+ * state), and the request is replayed later when the network returns.
8709
+ *
8710
+ * Right after queueing, the worker asks the page that issued the request who is
8711
+ * signed in (`getPrincipal` on {@link provideBackgroundSyncCredentials}) and
8712
+ * stamps the entry with that owner, without delaying the rejection. The work is
8713
+ * handed to `event.waitUntil` synchronously, so the worker stays alive for it
8714
+ * in every engine. Without the Background Sync API (every non-Chromium
8715
+ * browser), the queue is drained opportunistically whenever a `GET` goes
8716
+ * through the worker.
8223
8717
  *
8224
8718
  * @tempest-limits empty-catch — `registration.sync.register` is unavailable on
8225
8719
  * every non-Chromium browser and fails on a revoked background-sync permission.
@@ -8229,20 +8723,6 @@ export declare function inspectCaches(options?: {
8229
8723
  */
8230
8724
  export declare function installBackgroundSync(options?: InstallBackgroundSyncOptions): void;
8231
8725
 
8232
- /**
8233
- * Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)
8234
- * while offline and replay them when connectivity returns. A dependency-free
8235
- * take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.
8236
- *
8237
- * Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)
8238
- * when available, and also replays opportunistically on the next request as a
8239
- * fallback for browsers without it (e.g. Safari).
8240
- *
8241
- * @example
8242
- * import { installBackgroundSync } from "tempest-react-sdk/sw";
8243
- *
8244
- * installBackgroundSync({ match: (url) => url.pathname.startsWith("/api/") });
8245
- */
8246
8726
  /** Options for {@link installBackgroundSync}. */
8247
8727
  export declare interface InstallBackgroundSyncOptions {
8248
8728
  /**
@@ -8262,6 +8742,57 @@ export declare interface InstallBackgroundSyncOptions {
8262
8742
  * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.
8263
8743
  */
8264
8744
  periodicSyncTag?: string;
8745
+ /**
8746
+ * Header names stripped before a request is persisted — and stripped again on
8747
+ * replay, so entries written by an older version never go out with the
8748
+ * credential they stored. Default {@link DEFAULT_CREDENTIAL_HEADERS}; extend it
8749
+ * for a custom credential header (`[...DEFAULT_CREDENTIAL_HEADERS,
8750
+ * "x-tenant-key"]`). Passing `[]` restores the pre-0.77 behavior of
8751
+ * persisting and replaying every header, credentials included.
8752
+ */
8753
+ credentialHeaders?: readonly string[];
8754
+ /**
8755
+ * Prepare each queued request right before it is replayed: return the
8756
+ * request to send (typically with the current credentials applied) or
8757
+ * `null` to drop the entry for good. A thrown error keeps the entry pending.
8758
+ * The context carries the entry's owner, so a custom hook can apply the
8759
+ * same rule as the default.
8760
+ *
8761
+ * Default: only an `"unbound"` entry with no stripped credentials (and, under
8762
+ * `ownerPolicy: "lenient"`, an `"unknown"` one) is replayed as-is, without
8763
+ * asking. Every other entry — credentialed or not —
8764
+ * asks every open window client in parallel (see
8765
+ * {@link provideBackgroundSyncCredentials}) and picks the answer that fits:
8766
+ * - `"bound"`: the answer whose owner matches; none among valid answers →
8767
+ * dropped, never sent anonymously or as somebody else;
8768
+ * - `"unknown"`: only when every answer lacks a principal (an app that does
8769
+ * not use `getPrincipal`); a single answer with one → dropped, since a
8770
+ * stale tab without `getPrincipal` says nothing about who is signed in
8771
+ * next to it. This holds for an entry
8772
+ * with no credential header too, since the browser attaches the cookies of
8773
+ * whoever is signed in at replay time;
8774
+ * - `"unbound"`: any answer, preferring one without a principal.
8775
+ * Then `null` headers drop a credentialed entry, an answer that lacks one of
8776
+ * the stripped headers keeps it pending, and only the stripped names are
8777
+ * re-applied. No valid answer at all keeps it pending until
8778
+ * `maxRetentionMinutes` (and warns once per replay in development builds) —
8779
+ * so a page that never calls {@link provideBackgroundSyncCredentials} leaves
8780
+ * every entry it queued `"unknown"`, and none of them is replayed.
8781
+ */
8782
+ /**
8783
+ * How an `"unknown"` owner is handled on an entry with no stripped
8784
+ * credentials (see {@link BackgroundSyncOwnerPolicy}). Default `"strict"`.
8785
+ *
8786
+ * `"lenient"` restores the replay-as-is of 0.77.0 and earlier for an app without sign-in,
8787
+ * where no page ever reports a principal. Do not use it with a cookie
8788
+ * session: the browser attaches on replay the cookie of whoever is signed in
8789
+ * at that moment, so a mutation queued by A whose owner was never stamped
8790
+ * (issuing tab closed, empty `clientId`, stamp lost, legacy entry) goes out
8791
+ * with B's session. Entries with stripped credentials ask the pages either
8792
+ * way. Ignored when a custom `beforeReplay` is given.
8793
+ */
8794
+ ownerPolicy?: BackgroundSyncOwnerPolicy;
8795
+ beforeReplay?: (request: Request, context: BackgroundSyncReplayContext) => Request | null | Promise<Request | null>;
8265
8796
  }
8266
8797
 
8267
8798
  /**
@@ -8494,6 +9025,14 @@ export declare interface InstallPushHandlerOptions {
8494
9025
  * given runtime strategies. Non-matching requests are left untouched (no
8495
9026
  * `respondWith`), so a later {@link installPrecache} can handle them.
8496
9027
  *
9028
+ * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
9029
+ * route's `credentialHeaders`) is also left untouched unless its route sets
9030
+ * `cacheAuthenticated`, and a response with `Cache-Control: no-store`
9031
+ * (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
9032
+ * applies to every read, including the offline fallback: an expired entry is a
9033
+ * miss, never a last resort. Under `rangeRequests`, the full resource is cached
9034
+ * once (the `Range` header stripped) and each range is sliced from it.
9035
+ *
8497
9036
  * Register this BEFORE `installPrecache` so specific routes win over the
8498
9037
  * precache catch-all.
8499
9038
  *
@@ -9522,6 +10061,25 @@ export declare type LocalStorageOptions<T> = {
9522
10061
  deserialize?: (raw: string) => T;
9523
10062
  };
9524
10063
 
10064
+ /**
10065
+ * The default `localStorage` storage, which can also forget every record it
10066
+ * holds.
10067
+ */
10068
+ export declare interface LocalUploadStorage extends ResumableUploadStorage {
10069
+ /**
10070
+ * Forget every record under this storage's prefix — the call for
10071
+ * `onSessionEnd`, so the next account on the browser does not find the
10072
+ * previous one's upload URLs. Synchronous; a no-op without `localStorage`.
10073
+ *
10074
+ * Only values shaped like a record are removed, so a prefix without a
10075
+ * delimiter does not take the app's own keys with it. A run still going on
10076
+ * this prefix stops before its next chunk with
10077
+ * {@link ResumableUploadSupersededError} (`"cleared"`) instead of writing
10078
+ * its record back.
10079
+ */
10080
+ clear(): void;
10081
+ }
10082
+
9525
10083
  export declare interface LogEntry {
9526
10084
  level: LogLevel;
9527
10085
  message: string;
@@ -10639,6 +11197,38 @@ export declare interface NProgressState {
10639
11197
  active: boolean;
10640
11198
  }
10641
11199
 
11200
+ /**
11201
+ * A provider redirect that cannot be exchanged for a session.
11202
+ *
11203
+ * Thrown before any request leaves the browser, so a forged or truncated
11204
+ * callback never reaches the backend's exchange route.
11205
+ */
11206
+ export declare class OAuthCallbackError extends Error {
11207
+ /** Which part of the callback was wrong. */
11208
+ readonly reason: OAuthCallbackErrorReason;
11209
+ /** The provider's `error` param (e.g. `"access_denied"`), when `reason` is `"provider_error"`. */
11210
+ readonly providerError?: string;
11211
+ /**
11212
+ * @param reason - Which part of the callback was wrong.
11213
+ * @param providerError - The provider's `error` param, when it sent one.
11214
+ */
11215
+ constructor(reason: OAuthCallbackErrorReason, providerError?: string);
11216
+ }
11217
+
11218
+ /** Why {@link readOAuthCallbackParams} refused a callback URL. */
11219
+ export declare type OAuthCallbackErrorReason = "provider_error" | "missing_state" | "missing_code";
11220
+
11221
+ /** The two values a provider appends to the redirect URI on success. */
11222
+ export declare interface OAuthCallbackParams {
11223
+ /** Single-use authorization code. */
11224
+ code: string;
11225
+ /**
11226
+ * The CSRF `state` the backend minted when the flow started. Forwarded
11227
+ * verbatim: only the backend holds the value it must match.
11228
+ */
11229
+ state: string;
11230
+ }
11231
+
10642
11232
  export declare interface OAuthCredential {
10643
11233
  /** Provider-issued ID token (JWT). */
10644
11234
  idToken: string;
@@ -10720,19 +11310,68 @@ export declare interface OfflineIndicatorProps extends HTMLAttributes<HTMLDivEle
10720
11310
  children?: ReactNode;
10721
11311
  }
10722
11312
 
11313
+ /** Options for {@link OfflineQueryPersistence.clear}. */
11314
+ export declare interface OfflineQueryClearOptions {
11315
+ /**
11316
+ * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once
11317
+ * persistence has stopped and the snapshot is deleted — the one-call logout.
11318
+ * Default `false`: the client belongs to the app, which may want to keep
11319
+ * public queries (a catalog, feature flags) across sessions and drop only
11320
+ * the user-scoped keys with `removeQueries`.
11321
+ */
11322
+ resetClient?: boolean;
11323
+ }
11324
+
10723
11325
  /** Handle returned by {@link persistQueryClientOffline}. */
10724
11326
  export declare interface OfflineQueryPersistence {
10725
11327
  /**
10726
11328
  * Load the persisted snapshot into the `QueryClient`. Call once on boot,
10727
- * before rendering queries, so cached data is available offline.
11329
+ * before rendering queries, so cached data is available offline. A no-op
11330
+ * once {@link OfflineQueryPersistence.clear} has been called.
10728
11331
  */
10729
11332
  restore: () => Promise<void>;
10730
- /** Write the current cache snapshot immediately (bypassing the throttle). */
11333
+ /**
11334
+ * Write the current cache snapshot immediately (bypassing the throttle). A
11335
+ * no-op once {@link OfflineQueryPersistence.clear} has been called.
11336
+ */
10731
11337
  flush: () => Promise<void>;
10732
- /** Stop persisting and drop the pending write. */
11338
+ /** Stop persisting and drop the pending write. Idempotent. */
10733
11339
  unsubscribe: () => void;
10734
- /** Delete the persisted snapshot (e.g. on logout). */
10735
- clear: () => Promise<void>;
11340
+ /**
11341
+ * End the persisted session (e.g. on logout) and delete the snapshot.
11342
+ *
11343
+ * Terminal for this handle: it cancels the pending throttled write, stops
11344
+ * persisting cache changes, waits for any write already in flight and only
11345
+ * then deletes the row — so neither a write pending in the last throttle
11346
+ * window nor cache activity after logout (the next user's first query) can
11347
+ * re-persist the previous user's responses. Afterwards `restore`/`flush`
11348
+ * are no-ops; to persist a new session, call
11349
+ * {@link persistQueryClientOffline} again after login. In a dev build
11350
+ * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on
11351
+ * an ended handle logs one `console.warn` saying so, because a handle kept
11352
+ * at module scope would otherwise stop persisting in silence.
11353
+ *
11354
+ * Ordered against other handles on the same `databaseName` + `key`: until
11355
+ * this clear settles, a new handle's `restore()` and writes wait for it, so
11356
+ * a handle created on login without awaiting the logout neither hydrates
11357
+ * the previous snapshot nor has its first write deleted.
11358
+ *
11359
+ * If the delete rejects, `resetClient` still clears the client and the
11360
+ * rejection reaches the caller.
11361
+ *
11362
+ * The in-memory `QueryClient` is cleared only with `resetClient: true`, and
11363
+ * then only after persistence has stopped, so the reset itself cannot
11364
+ * trigger a write. It is opt-in because the app owns the client and the
11365
+ * shape of its logout: it may keep public queries and drop only the
11366
+ * user-scoped keys with `removeQueries`. Because persistence is already
11367
+ * off, that manual call can come before or after this one.
11368
+ *
11369
+ * Idempotent and safe to call concurrently; every call deletes the row
11370
+ * again.
11371
+ *
11372
+ * @param options - `resetClient` also clears the in-memory `QueryClient`.
11373
+ */
11374
+ clear: (options?: OfflineQueryClearOptions) => Promise<void>;
10736
11375
  }
10737
11376
 
10738
11377
  /** Options for {@link persistQueryClientOffline}. */
@@ -11697,7 +12336,23 @@ export declare interface PeerMesh {
11697
12336
  applyQuality: (quality: MeshQuality) => Promise<void>;
11698
12337
  /** Replace the ICE servers used for links opened from now on. */
11699
12338
  setIceServers: (servers: RTCIceServer[]) => void;
11700
- /** Close every link and report `closed`. */
12339
+ /**
12340
+ * Close every link, forget the local tracks and report `closed` — for good.
12341
+ *
12342
+ * Stopping is terminal. The signalling socket usually outlives the call, so
12343
+ * an offer still in flight from a former participant, or from anyone the
12344
+ * server relays to this user, keeps reaching {@link PeerMesh.accept} after
12345
+ * the hang-up. A stopped mesh drops it: no connection is opened, nothing is
12346
+ * answered and no track is published, so a call that reports `closed` never
12347
+ * sends the microphone or camera again. The same holds for `addPeer`,
12348
+ * `setLocalTrack` and `applyQuality`, and for every negotiation that was
12349
+ * already awaiting the browser when `stop()` ran. Calling it twice is a
12350
+ * no-op.
12351
+ *
12352
+ * The tracks themselves are not stopped: they belong to the caller, who may
12353
+ * still be showing them in a preview or reusing them on the next call. To
12354
+ * join again, create a new mesh.
12355
+ */
11701
12356
  stop: () => void;
11702
12357
  /** The peers as the mesh currently sees them. */
11703
12358
  readonly peers: MeshPeer[];
@@ -11820,11 +12475,18 @@ export declare interface PermissionsFromTokenOptions {
11820
12475
  * @param options - The client plus storage/throttle configuration.
11821
12476
  * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.
11822
12477
  *
12478
+ * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):
12479
+ * a logout that only deleted the row used to be undone by the pending write or
12480
+ * by the next cache event, restoring the previous user's data on the next boot
12481
+ * (#440).
12482
+ *
11823
12483
  * @example
11824
12484
  * const persistence = persistQueryClientOffline({ queryClient });
11825
- * await persistence.restore(); // before first render
11826
- * // ... later, on logout:
11827
- * await persistence.clear();
12485
+ * await persistence.restore();
12486
+ *
12487
+ * async function logout(): Promise<void> {
12488
+ * await persistence.clear({ resetClient: true });
12489
+ * }
11828
12490
  */
11829
12491
  export declare function persistQueryClientOffline(options: OfflineQueryPersistenceOptions): OfflineQueryPersistence;
11830
12492
 
@@ -12170,6 +12832,57 @@ export declare type ProgressVariant = "primary" | "success" | "warning" | "dange
12170
12832
  */
12171
12833
  export declare function projectMercator(coord: Coordinate): MercatorPoint;
12172
12834
 
12835
+ /**
12836
+ * Answer the worker's questions about queued requests (see `beforeReplay` on
12837
+ * {@link installBackgroundSync}). Install it once in every page, next to your
12838
+ * auth setup; it reads the token at replay time, so the queue never has to store
12839
+ * it. Without it, a queued request that carried a credential header is never
12840
+ * replayed.
12841
+ *
12842
+ * `getHeaders` must return **every** credential header the app sends (the worker
12843
+ * keeps an entry pending when one it stripped is missing), or `null` when there
12844
+ * is no session — the worker then drops the entry instead of replaying it
12845
+ * anonymously. A throw from `getHeaders` or `getPrincipal` is answered as an
12846
+ * explicit `{ error: true }`. The worker only applies headers whose names the
12847
+ * original request carried, so returning `Authorization` for every URL never
12848
+ * leaks it to a URL that did not receive it in the first place.
12849
+ *
12850
+ * Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
12851
+ * the issuing tab right after queueing, asks every tab again before replaying,
12852
+ * and drops the entry when no signed-in tab is its owner.
12853
+ *
12854
+ * @param getHeaders - Resolves the headers for one queued request at replay.
12855
+ * @param options - `getPrincipal` binds entries to their owner.
12856
+ * @returns A function that removes the listener. A no-op outside the browser.
12857
+ *
12858
+ * @example
12859
+ * import { provideBackgroundSyncCredentials } from "tempest-react-sdk/sw";
12860
+ *
12861
+ * const stop = provideBackgroundSyncCredentials(
12862
+ * () => {
12863
+ * const token = localStorage.getItem("token");
12864
+ * return token ? { Authorization: `Bearer ${token}` } : null;
12865
+ * },
12866
+ * { getPrincipal: () => localStorage.getItem("userId") },
12867
+ * );
12868
+ */
12869
+ export declare function provideBackgroundSyncCredentials(getHeaders: BackgroundSyncCredentialsProvider, options?: ProvideBackgroundSyncCredentialsOptions): () => void;
12870
+
12871
+ /** Options for {@link provideBackgroundSyncCredentials}. */
12872
+ export declare interface ProvideBackgroundSyncCredentialsOptions {
12873
+ /**
12874
+ * Who is signed in right now — a stable, non-secret id such as the user id;
12875
+ * `null`/`undefined` when nobody is. Read when a request is queued and again
12876
+ * when it is replayed: an entry is only sent while the same principal is
12877
+ * signed in, and dropped otherwise, so a mutation queued by one account is
12878
+ * never sent with another account's credentials even when the app forgets
12879
+ * `clearSessionData()` on logout. Only a SHA-256 fingerprint reaches the
12880
+ * queue, never the value itself. A throw is answered as an explicit error,
12881
+ * which leaves the owner unknown (fail closed).
12882
+ */
12883
+ getPrincipal?: () => string | null | undefined | Promise<string | null | undefined>;
12884
+ }
12885
+
12173
12886
  /**
12174
12887
  * One page of the server delta pull.
12175
12888
  *
@@ -12568,6 +13281,50 @@ export declare function readAvailableOutgoingKbps(report: RTCStatsReport): numbe
12568
13281
  */
12569
13282
  export declare function readDeviceProfile(): DeviceProfile;
12570
13283
 
13284
+ /** Options for {@link readOAuthCallbackParams}. */
13285
+ export declare interface ReadOAuthCallbackOptions {
13286
+ /**
13287
+ * Remove `code` and `state` from the address bar after reading them, with
13288
+ * `history.replaceState` — other params, the hash and `history.state` are
13289
+ * kept. Default: `true` when reading `window.location`, `false` when an
13290
+ * explicit `search` is passed (there is no URL of yours to clean then).
13291
+ */
13292
+ scrubUrl?: boolean;
13293
+ }
13294
+
13295
+ /**
13296
+ * Read `code` and `state` from a provider's redirect, refusing an incomplete one.
13297
+ *
13298
+ * The `state` is what ties the callback to the browser that started the flow.
13299
+ * Without it, an attacker sends the victim to `/callback?code=<attacker's code>`
13300
+ * and the victim ends up signed in to the attacker's account — a login CSRF.
13301
+ * The old recipe read only `code` and posted `{ code }`, so the `state` never
13302
+ * reached a backend that could compare it (#449). This reader makes dropping it
13303
+ * impossible: it returns both or throws.
13304
+ *
13305
+ * It does **not** compare the state. In the `tempest-fastapi-sdk` flow the
13306
+ * backend mints it in `GET {prefix}/oauth/{provider}/login`, keeps it in an
13307
+ * `HttpOnly` cookie bound to the provider, and checks it with
13308
+ * `hmac.compare_digest` in the callback (single use, `AUTH_OAUTH_STATE_TTL_SECONDS`
13309
+ * expiry). The browser cannot read that cookie, so any check here would be
13310
+ * theatre; forwarding the value is the browser's whole job.
13311
+ *
13312
+ * When it reads `window.location`, it also removes `code` and `state` from the
13313
+ * address bar (see {@link ReadOAuthCallbackOptions.scrubUrl}) — before
13314
+ * validating, so a refused callback is cleaned too.
13315
+ *
13316
+ * @param search - The query string or params to read. Default: `window.location.search`.
13317
+ * @param options - Whether to scrub the URL.
13318
+ * @returns The `code` and `state`.
13319
+ * @throws {OAuthCallbackError} `"provider_error"` when the provider sent `error`
13320
+ * (the user declined, for instance), `"missing_state"` without `state`,
13321
+ * `"missing_code"` without `code`.
13322
+ *
13323
+ * @example
13324
+ * const { code, state } = readOAuthCallbackParams("?code=4%2F0A&state=xyz");
13325
+ */
13326
+ export declare function readOAuthCallbackParams(search?: string | URLSearchParams, options?: ReadOAuthCallbackOptions): OAuthCallbackParams;
13327
+
12571
13328
  /**
12572
13329
  * Read what the encoder says is holding the picture back.
12573
13330
  *
@@ -12846,6 +13603,26 @@ export declare interface ReorderPayload {
12846
13603
  to: number;
12847
13604
  }
12848
13605
 
13606
+ /**
13607
+ * What {@link ApiClientConfig.refresh} and {@link ApiClientConfig.onUnauthorized}
13608
+ * learn about the attempt that came back `401`: the credential and the session it
13609
+ * left under, both read when that attempt was sent.
13610
+ */
13611
+ export declare interface RequestAuthContext {
13612
+ /**
13613
+ * The access token the rejected attempt was sent with, read from `getToken`
13614
+ * when it left — or `null` when it carried none (`skipAuth`, no token yet,
13615
+ * or no `getToken` configured).
13616
+ */
13617
+ token: string | null;
13618
+ /**
13619
+ * What `getSessionEpoch` returned when the attempt left, or `null` when it
13620
+ * is not configured. Differing from the current value means the attempt
13621
+ * belongs to a session that has since ended.
13622
+ */
13623
+ epoch: number | null;
13624
+ }
13625
+
12849
13626
  export declare interface RequestOptions extends Omit<RequestInit, "body"> {
12850
13627
  body?: unknown;
12851
13628
  params?: Record<string, string | number | boolean | undefined | null>;
@@ -13045,6 +13822,29 @@ export declare interface ResumableUploadOptions {
13045
13822
  headers?: Record<string, string>;
13046
13823
  /** Returns the current bearer token, read before each request. */
13047
13824
  getToken?: () => string | null | undefined;
13825
+ /**
13826
+ * Who is signed in — a user id, or `null`/`undefined` for nobody — read when
13827
+ * `start()`/`resume()` attaches to the upload. The same getter the error
13828
+ * reporter and `provideBackgroundSyncCredentials` take.
13829
+ *
13830
+ * The resume record is stamped with a one-way fingerprint of it, and a
13831
+ * record stamped by anyone else — another user, nobody, or no stamp at all
13832
+ * — is deleted instead of resumed: its upload URL is not probed, and its
13833
+ * `Idempotency-Key` is never replayed. Without it, the record is bound to
13834
+ * the file alone (endpoint, name, size, type, mtime), so after user A
13835
+ * signed out leaving an upload half-done, user B picking an identical file
13836
+ * sent `HEAD`/`PATCH` to A's upload URL with B's token, and on refusal
13837
+ * replayed A's `Idempotency-Key` with B's token (#479). Pass it on every
13838
+ * app with more than one account per browser, and also clear the storage
13839
+ * at session end (`createLocalUploadStorage().clear()` in `onSessionEnd`).
13840
+ *
13841
+ * It is read again before every chunk and every record write: when it
13842
+ * names someone else mid-run, the run stops with
13843
+ * {@link ResumableUploadSupersededError} (`"principal"`) before the new
13844
+ * account's token reaches the previous one's upload URL. Without it, call
13845
+ * `abort()` on the uploads in progress from `onSessionEnd`.
13846
+ */
13847
+ getPrincipal?: () => string | number | null | undefined;
13048
13848
  /**
13049
13849
  * Origins besides `endpoint`'s that may receive the `getToken` credential.
13050
13850
  *
@@ -13112,6 +13912,13 @@ export declare interface ResumableUploadRecord {
13112
13912
  idempotencyKey: string;
13113
13913
  /** Epoch ms of the last write, so an app can sweep stale records. */
13114
13914
  updatedAt: number;
13915
+ /**
13916
+ * One-way fingerprint of the principal that created the upload (`"p:<hex>"`),
13917
+ * `null` for an anonymous one. Absent when the upload ran without
13918
+ * {@link ResumableUploadOptions.getPrincipal}, and on records written before
13919
+ * 0.78.0.
13920
+ */
13921
+ owner?: string | null;
13115
13922
  }
13116
13923
 
13117
13924
  /** What a finished upload resolves with. */
@@ -13146,18 +13953,65 @@ export declare interface ResumableUploadStorage {
13146
13953
  delete(key: string): Promise<void> | void;
13147
13954
  }
13148
13955
 
13956
+ /**
13957
+ * Thrown by `start()`/`resume()` when the run can no longer go on as the
13958
+ * account that started it.
13959
+ *
13960
+ * - `"principal"` — `getPrincipal` names someone else than when the run
13961
+ * attached: the next chunk would carry the new account's token to the
13962
+ * previous account's upload URL, and the record would be stamped for the
13963
+ * wrong owner.
13964
+ * - `"cleared"` — `createLocalUploadStorage().clear()` ran for this storage's
13965
+ * prefix (typically in `onSessionEnd`): writing the record back would undo
13966
+ * the clear.
13967
+ *
13968
+ * Not a pause: the run cannot be resumed as the new account. The state moves
13969
+ * to `"error"`, and the previous owner's record is left as it was.
13970
+ */
13971
+ export declare class ResumableUploadSupersededError extends Error {
13972
+ /** Why the run stopped. */
13973
+ readonly reason: "principal" | "cleared";
13974
+ /**
13975
+ * @param reason - Why the run stopped.
13976
+ */
13977
+ constructor(reason: "principal" | "cleared");
13978
+ }
13979
+
13149
13980
  /**
13150
13981
  * Run `factory()` with exponential backoff. Each attempt awaits an
13151
13982
  * increasing delay capped at `maxDelay`. Throws the last error if every
13152
13983
  * attempt fails.
13153
13984
  *
13985
+ * `retries` is the **total** number of attempts, never fewer than one — see
13986
+ * {@link RetryOptions.retries}. `retries: 5` means one call plus four replays.
13987
+ *
13988
+ * @param factory - Starts one attempt; called again for every replay.
13989
+ * @param options - Attempt count, backoff and the retry predicate.
13990
+ * @returns What the first successful attempt resolved with.
13991
+ * @throws The error of the last attempt, or an `AbortError` once `signal` aborts.
13992
+ *
13154
13993
  * @example
13155
13994
  * const data = await retry(() => api.get("/flaky"), { retries: 5 });
13156
13995
  */
13157
13996
  export declare function retry<T>(factory: () => Promise<T>, options?: RetryOptions): Promise<T>;
13158
13997
 
13159
13998
  export declare interface RetryOptions {
13160
- /** Max attempts (including the first). Default: 3. */
13999
+ /**
14000
+ * Total attempts, the first one included. Default: 3 — one call plus two
14001
+ * replays.
14002
+ *
14003
+ * The first attempt always runs: `0`, a negative number or a fraction below
14004
+ * one all mean "a single attempt, no replay", and that attempt's error is the
14005
+ * one the promise rejects with. A fraction is rounded down; `NaN` falls back
14006
+ * to the default.
14007
+ *
14008
+ * The name reads like "replays after the first", but every surface that
14009
+ * forwards it has always counted totals — {@link createApiClient}'s `retry`,
14010
+ * `createResumableUpload` ("5 attempts"), and `lazyWithRetry`'s own
14011
+ * `retries`. Switching to "replays" would add one request to every caller
14012
+ * that passes a number, which no change of default can hide, so the count
14013
+ * stays a total and only the values that ran nothing were fixed (#486).
14014
+ */
13161
14015
  retries?: number;
13162
14016
  /** Initial backoff in ms. Doubles each attempt, capped at `maxDelay`. Default: 300. */
13163
14017
  initialDelay?: number;
@@ -13368,6 +14222,38 @@ export declare interface RuntimeRoute {
13368
14222
  * resource is cached once (the `Range` header is stripped before caching).
13369
14223
  */
13370
14224
  rangeRequests?: boolean;
14225
+ /**
14226
+ * Opt in to caching requests that carry a credential header (see
14227
+ * `credentialHeaders`), and responses marked `Cache-Control: private`.
14228
+ * Default `false`.
14229
+ *
14230
+ * Cache Storage is keyed by URL and outlives the session: without this
14231
+ * gate, a `GET /api/me` stored for one account was served to the next
14232
+ * account on the same browser whenever the network failed or exceeded
14233
+ * `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With
14234
+ * the default, credentialed requests bypass the route entirely and reach
14235
+ * the network as if no service worker were installed.
14236
+ *
14237
+ * When enabled, every stored entry gets `Vary` on each credential header the
14238
+ * request carried, so the Cache API only matches it for the same credential —
14239
+ * another account (or no account) misses instead of reading it. A token
14240
+ * rotated while online simply repopulates the entry. Pair it with
14241
+ * `clearSessionData()` on logout, which removes the entries themselves.
14242
+ * `Cache-Control: no-store` is never cached, opt-in or not.
14243
+ *
14244
+ * A cookie session is invisible here: the worker cannot see `Cookie`, and
14245
+ * gating on `request.credentials` would switch off caching for every
14246
+ * same-origin image and font. A cookie-authenticated API stays private only
14247
+ * through `Cache-Control: private`/`no-store` on its responses and
14248
+ * `clearSessionData()` on logout.
14249
+ */
14250
+ cacheAuthenticated?: boolean;
14251
+ /**
14252
+ * Header names that mark a request as credentialed. Default
14253
+ * `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,
14254
+ * `x-auth-token`, …) — the same list the background-sync queue strips.
14255
+ */
14256
+ credentialHeaders?: readonly string[];
13371
14257
  }
13372
14258
 
13373
14259
  /**
@@ -13392,7 +14278,7 @@ export declare interface RuntimeRoute {
13392
14278
  *
13393
14279
  * // Register specific routes FIRST so they win over the precache catch-all.
13394
14280
  * installRuntimeCache([
13395
- * { match: /\/api\//, strategy: "network-first", cacheName: "api", maxAgeSeconds: 300 },
14281
+ * { match: /\/api\//, strategy: "network-first", cacheName: "tempest-api", maxAgeSeconds: 300 },
13396
14282
  * ]);
13397
14283
  * installPrecache();
13398
14284
  */
@@ -13421,6 +14307,77 @@ export declare interface SafeAreaProps extends HTMLAttributes<HTMLDivElement> {
13421
14307
  children?: ReactNode;
13422
14308
  }
13423
14309
 
14310
+ /**
14311
+ * The URL to fetch or download an attachment from, or `null`.
14312
+ *
14313
+ * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster
14314
+ * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href
14315
+ * download>`, and the `download` attribute does not stop a `javascript:` href
14316
+ * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme
14317
+ * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an
14318
+ * attachment is bytes, not a contact.
14319
+ *
14320
+ * The scheme is judged on the normalized form, but the URL comes back **as given**:
14321
+ * normalizing deletes every space, and `/files/relatório final.pdf` is a real
14322
+ * attachment path. Returning the original is safe because the normalization only
14323
+ * removes characters, a superset of what the browser's URL parser strips, so it
14324
+ * can never hide a scheme the browser would see.
14325
+ *
14326
+ * @example
14327
+ * const href = safeAttachmentUrl(attachment.url);
14328
+ * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;
14329
+ *
14330
+ * @param raw - URL as the sender supplied it.
14331
+ * @returns A safe URL, or `null` when it must not reach an `href`.
14332
+ */
14333
+ export declare function safeAttachmentUrl(raw: string): string | null;
14334
+
14335
+ /**
14336
+ * The `src` to render for an image, or `null`.
14337
+ *
14338
+ * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.
14339
+ *
14340
+ * @example
14341
+ * safeImageUrl("data:image/png;base64,iVBORw0KGgo="); // kept
14342
+ * safeImageUrl("data:image/svg+xml;base64,PHN2Zz4="); // null
14343
+ *
14344
+ * @param raw - URL as written in the Markdown.
14345
+ * @returns A safe src, or `null`.
14346
+ */
14347
+ export declare function safeImageUrl(raw: string): string | null;
14348
+
14349
+ /**
14350
+ * URL vetting for every `href` and `src` the SDK fills from somebody else's data:
14351
+ * links and images in rendered Markdown, and the file row of a chat attachment.
14352
+ *
14353
+ * This is a security boundary. Text that arrives from a user — a comment, a ticket
14354
+ * description, a chat attachment — reaches the DOM as a real `href`, and
14355
+ * `javascript:alert(1)` in a link is script execution on click. React 19 replaces
14356
+ * a `javascript:` href with a throwing stub, but the peer range admits React 18,
14357
+ * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote
14358
+ * `href="javascript:..."` verbatim, and clicking it ran the script in Chromium 153
14359
+ * and Firefox 155 — `download` on the anchor did not stop it (#447). So the
14360
+ * scheme is checked against an allowlist rather than a blocklist: a blocklist has
14361
+ * to enumerate `javascript:`, `JaVaScRiPt:`, `java\tscript:`, `&#106;avascript:`
14362
+ * and whatever comes next, and misses the one nobody thought of.
14363
+ */
14364
+ /**
14365
+ * The href to render, or `null` when the URL must not be linked.
14366
+ *
14367
+ * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry
14368
+ * a scheme, so they cannot execute anything, and dropping them would break every
14369
+ * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is
14370
+ * kept for the same reason.
14371
+ *
14372
+ * @example
14373
+ * safeLinkUrl("https://x.dev"); // "https://x.dev"
14374
+ * safeLinkUrl("java\tscript:alert(1)"); // null
14375
+ *
14376
+ * @param raw - URL as written in the Markdown.
14377
+ * @returns A safe href, or `null`.
14378
+ */
14379
+ export declare function safeLinkUrl(raw: string): string | null;
14380
+
13424
14381
  /** A schema exposing zod's `.safeParse`, including versions older than `~standard`. */
13425
14382
  export declare interface SafeParseSchemaLike<T> {
13426
14383
  readonly safeParse: (value: unknown) => {
@@ -13910,6 +14867,35 @@ export declare interface SequentialScaleOptions {
13910
14867
  ordinal?: boolean;
13911
14868
  }
13912
14869
 
14870
+ /**
14871
+ * Thrown when the session that started an operation is no longer the current
14872
+ * one by the time it settles: a `refresh` hook (rethrown by
14873
+ * {@link createApiClient}), or a `createTempestAuth` `login()` whose session
14874
+ * was replaced while it waited on the previous session's teardown.
14875
+ *
14876
+ * A refresh is a network round trip, and the session can end or change hands
14877
+ * during it: the user signs out, or signs out and another account signs in. The
14878
+ * tokens that refresh brings back belong to the session that ended, so they are
14879
+ * discarded instead of installed, and the request that was waiting on them is
14880
+ * not replayed — replaying it would send it as whoever is signed in now.
14881
+ *
14882
+ * This is deliberately not an unauthorized outcome. The client does **not** call
14883
+ * `onUnauthorized` for it: that callback ends the session, and the current
14884
+ * session — possibly a different account that just signed in — did nothing
14885
+ * wrong. Treat it like an aborted request: drop the result, show nothing.
14886
+ *
14887
+ * @example
14888
+ * try {
14889
+ * await api.get("/api/orders");
14890
+ * } catch (err) {
14891
+ * if (err instanceof SessionSupersededError) return;
14892
+ * throw err;
14893
+ * }
14894
+ */
14895
+ export declare class SessionSupersededError extends Error {
14896
+ constructor();
14897
+ }
14898
+
13913
14899
  /**
13914
14900
  * Send an element's audio to a specific output device.
13915
14901
  *
@@ -15158,9 +16144,23 @@ export declare interface TempestAuth<TUser, TCredentials> {
15158
16144
  api: ApiClient;
15159
16145
  /** Authenticate, store the session, and resolve the user (or null). */
15160
16146
  login: (credentials: TCredentials) => Promise<TUser | null>;
15161
- /** Clear the session (and the stored refresh token). */
15162
- logout: () => void;
15163
- /** Refresh the access token (deduplicated across concurrent callers). */
16147
+ /**
16148
+ * End the session: supersede any refresh in flight, clear the store and the
16149
+ * stored refresh token, then run `onSessionEnd`.
16150
+ *
16151
+ * The store is cleared synchronously, before the first `await`, so callers
16152
+ * that ignore the promise see the signed-out state at once. The promise
16153
+ * resolves when `onSessionEnd` settled — and, with `logoutPath`, when the
16154
+ * server-side revocation settled too — and rejects with the hook's error;
16155
+ * a failed revocation never rejects it.
16156
+ */
16157
+ logout: () => Promise<void>;
16158
+ /**
16159
+ * Refresh the access token (deduplicated across concurrent callers).
16160
+ *
16161
+ * @throws {SessionSupersededError} When the session ended or changed while
16162
+ * the refresh was in flight; its tokens were discarded.
16163
+ */
15164
16164
  refresh: () => Promise<void>;
15165
16165
  /** The current access token, or null. */
15166
16166
  getToken: () => string | null;
@@ -15233,6 +16233,35 @@ export declare interface TempestRouteObject {
15233
16233
  caseSensitive?: boolean;
15234
16234
  }
15235
16235
 
16236
+ /**
16237
+ * What {@link CreateTempestAuthOptions.onSessionEnd} receives about the session
16238
+ * that just ended.
16239
+ */
16240
+ declare interface TempestSessionEnd<TUser> {
16241
+ /**
16242
+ * Why it ended: `"logout"` for an explicit `logout()`, `"expired"` when the
16243
+ * client ended it on its own — the refresh failed, or the request replayed
16244
+ * after a refresh still came back `401` — and `"switch"` when `login()`
16245
+ * succeeded for another set of credentials while this session was live.
16246
+ */
16247
+ reason: "logout" | "expired" | "switch";
16248
+ /** The user of the ended session, as the store held it. */
16249
+ user: TUser | null;
16250
+ /** The access token of the ended session, or `null` when it had none. */
16251
+ token: string | null;
16252
+ /**
16253
+ * A client that sends the ended session's access token — the store has
16254
+ * already been cleared, so `auth.api` no longer does.
16255
+ *
16256
+ * Use it for the server-side half of the teardown, such as deleting this
16257
+ * browser's push subscription. It never refreshes and never ends a session:
16258
+ * a `401` here just rejects. On `"logout"` the token is usually still
16259
+ * valid; on `"expired"` the server has already refused it, so expect the
16260
+ * call to fail and keep the local half of the teardown independent of it.
16261
+ */
16262
+ api: ApiClient;
16263
+ }
16264
+
15236
16265
  /** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */
15237
16266
  export declare interface TempestTokenResponse {
15238
16267
  /** The bearer access token. */
@@ -15434,6 +16463,16 @@ export declare interface TimePickerProps {
15434
16463
 
15435
16464
  export { To }
15436
16465
 
16466
+ /**
16467
+ * Horizontal alignment of a toast's title and description.
16468
+ *
16469
+ * `"start"` is the default because a toast is read as a sentence beside a close
16470
+ * button: left-aligned lines share one edge with the icon and the stack, so a
16471
+ * two-line description stays scannable. `"center"` exists for the short
16472
+ * celebratory toast (an achievement with a badge) where the text is one line.
16473
+ */
16474
+ export declare type ToastAlign = "start" | "center";
16475
+
15437
16476
  export declare interface ToastApi {
15438
16477
  show: (options: ToastOptions) => string;
15439
16478
  dismiss: (id: string) => void;
@@ -15450,6 +16489,30 @@ export declare interface ToastOptions {
15450
16489
  variant?: ToastVariant;
15451
16490
  /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */
15452
16491
  duration?: number;
16492
+ /**
16493
+ * Leading slot rendered before the text, e.g. `<img src="/badge.svg" alt="" />`.
16494
+ *
16495
+ * It is an app asset, so it takes any `ReactNode` — but it is a slot for an
16496
+ * **icon**, not for a control. The wrapper is `inert`, so a button or link put
16497
+ * here is neither focusable nor clickable; actions belong outside the toast.
16498
+ *
16499
+ * The slot is decorative (`aria-hidden`) unless {@link ToastOptions.iconLabel}
16500
+ * is given: most icons repeat what the title already says, and an unlabelled
16501
+ * image inside the live region would otherwise be announced as "image" or by
16502
+ * its file name. An `<img>` or `<svg>` anywhere inside it is capped at 32px and
16503
+ * keeps its aspect ratio. A node that renders nothing (`null`, a boolean, `""`,
16504
+ * `[]`, an empty fragment) counts as no icon, so the toast keeps two columns.
16505
+ */
16506
+ icon?: ReactNode;
16507
+ /**
16508
+ * Accessible name for {@link ToastOptions.icon} when the icon carries meaning
16509
+ * the text does not. With it the slot becomes `role="img"` with this label and
16510
+ * is announced together with the toast; without it the slot is hidden from
16511
+ * assistive technology.
16512
+ */
16513
+ iconLabel?: string;
16514
+ /** Text alignment of title and description. Default `"start"`; see {@link ToastAlign}. */
16515
+ align?: ToastAlign;
15453
16516
  }
15454
16517
 
15455
16518
  export declare type ToastPosition = "top-right" | "top-left" | "top-center" | "bottom-right" | "bottom-left" | "bottom-center";
@@ -15473,7 +16536,9 @@ export declare type ToastVariant = "success" | "warning" | "error" | "info";
15473
16536
  * Serialize rows to CSV text, RFC 4180 style.
15474
16537
  *
15475
16538
  * Rows are separated by `\r\n` — the RFC's terminator and the one Excel is least
15476
- * surprised by. A row is emitted for the header even when `rows` is empty, so the
16539
+ * surprised by. Text that a spreadsheet would evaluate as a formula is written
16540
+ * with a leading `'` unless `neutralizeFormulas` is `false` (see
16541
+ * {@link CsvOptions.neutralizeFormulas}). A row is emitted for the header even when `rows` is empty, so the
15477
16542
  * person who opens the file sees which columns they asked for instead of a blank
15478
16543
  * document.
15479
16544
  *
@@ -15486,7 +16551,7 @@ export declare type ToastVariant = "success" | "warning" | "error" | "info";
15486
16551
  *
15487
16552
  * @param rows - The rows to export.
15488
16553
  * @param columns - Columns, in the order they should appear.
15489
- * @param options - Delimiter and BOM.
16554
+ * @param options - Delimiter, BOM and formula neutralization.
15490
16555
  * @returns The complete file contents.
15491
16556
  */
15492
16557
  export declare function toCsv<T>(rows: readonly T[], columns: readonly CsvColumn<T>[], options?: CsvOptions): string;
@@ -17699,18 +18764,26 @@ export declare interface UseNotificationInboxResult {
17699
18764
  * `/callback` routes that receive provider redirects and need to swap a
17700
18765
  * code/token for an app session via the backend.
17701
18766
  *
17702
- * Survives React StrictMode double-mounting in dev — uses a ref guard to
17703
- * ensure `exchange` runs once.
18767
+ * Runs `exchange` once per logical mount and keeps its result. StrictMode's
18768
+ * simulated unmount/remount in dev and a parent re-render with inline
18769
+ * callbacks used to cancel the only exchange and skip the second, leaving the
18770
+ * hook `"pending"` forever with the single-use code already spent. Now the
18771
+ * options live in a latest-value ref, the effect has no dependencies, and the
18772
+ * result is dropped only when the component is still unmounted when it lands.
18773
+ * The newest `onSuccess`/`onError` are the ones called.
18774
+ *
18775
+ * For a provider redirect, the exchange must carry the `state` along with the
18776
+ * `code`, or no backend can tell a forged callback from a real one (login
18777
+ * CSRF). `exchangeOAuthCallback` does that against a
18778
+ * `tempest-fastapi-sdk` backend; `readOAuthCallbackParams` returns both
18779
+ * values (or throws) for any other backend.
17704
18780
  *
17705
18781
  * @example
17706
18782
  * function OAuthCallback() {
17707
- * const { loading, error } = useOAuthCallback({
17708
- * exchange: async () => {
17709
- * const code = new URLSearchParams(location.search).get("code")!;
17710
- * return api.post("/auth/google/exchange", { body: { code } });
17711
- * },
17712
- * onSuccess: ({ token, user }) => {
17713
- * useAuthStore.getState().setSession({ user, token });
18783
+ * const { loading, error } = useOAuthCallback<LoginResponse>({
18784
+ * exchange: () => exchangeOAuthCallback<LoginResponse>(api, "google"),
18785
+ * onSuccess: ({ user_id, access_token }) => {
18786
+ * useAuthStore.getState().setSession({ user: { id: user_id }, token: access_token });
17714
18787
  * navigate("/dashboard", { replace: true });
17715
18788
  * },
17716
18789
  * onError: () => navigate("/login?error=oauth", { replace: true }),
@@ -19945,10 +21018,29 @@ export declare class WebPushClient {
19945
21018
  */
19946
21019
  subscribe(): Promise<PushSubscription>;
19947
21020
  /**
19948
- * Cancel the current subscription. Calls `onUnsubscribe` first (so the
19949
- * backend can purge the record) and then `subscription.unsubscribe()`.
19950
- *
19951
- * @returns True when an active subscription was removed, false otherwise.
21021
+ * Cancel the current subscription: `onUnsubscribe` first, so the backend
21022
+ * can purge the record while the endpoint is known, then
21023
+ * `subscription.unsubscribe()` — **always**, even when `onUnsubscribe`
21024
+ * failed.
21025
+ *
21026
+ * Why the browser is released anyway: the two failure modes are not
21027
+ * symmetric. A backend row left behind costs one send to a dead endpoint,
21028
+ * which the push service answers with `404`/`410` and the backend deletes.
21029
+ * A browser subscription left behind keeps delivering this account's
21030
+ * notifications to whoever uses the browser next — and the backend call
21031
+ * fails precisely when that matters, at the end of a session, when the
21032
+ * token that authenticates it is already gone. Before 0.77.0 a rejected
21033
+ * `onUnsubscribe` aborted here and the browser stayed subscribed (#445).
21034
+ * The endpoint is not lost either way: it is read before both calls and
21035
+ * travels in the error.
21036
+ *
21037
+ * @returns True when an active subscription was removed, false when there
21038
+ * was none or the browser refused.
21039
+ * @throws {WebPushUnsubscribeError} When `onUnsubscribe` failed — thrown
21040
+ * after the browser subscription was released. When the browser call also
21041
+ * threw, `browserUnsubscribed` is `false` and `cause` is an
21042
+ * `AggregateError` holding both errors, backend first, so the endpoint the
21043
+ * error carries is never traded for a bare `DOMException`.
19952
21044
  */
19953
21045
  unsubscribe(): Promise<boolean>;
19954
21046
  }
@@ -19961,7 +21053,12 @@ export declare interface WebPushClientConfig {
19961
21053
  * the host app decides the endpoint and how the payload is serialized.
19962
21054
  */
19963
21055
  onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;
19964
- /** Remove the subscription from the backend. Called before `subscription.unsubscribe()`. */
21056
+ /**
21057
+ * Remove the subscription from the backend. Called before
21058
+ * `subscription.unsubscribe()`, with the subscription's JSON — key the
21059
+ * delete by its `endpoint`. A rejection no longer keeps the browser
21060
+ * subscribed: see {@link WebPushClient.unsubscribe}.
21061
+ */
19965
21062
  onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;
19966
21063
  /**
19967
21064
  * Override how the service worker registration is obtained.
@@ -19975,6 +21072,28 @@ export declare class WebPushPermissionDeniedError extends Error {
19975
21072
  constructor();
19976
21073
  }
19977
21074
 
21075
+ /**
21076
+ * Thrown by {@link WebPushClient.unsubscribe} when `onUnsubscribe` failed. The
21077
+ * browser subscription was still released — `browserUnsubscribed` says whether
21078
+ * the browser confirmed it — so the only thing left behind is the backend row.
21079
+ *
21080
+ * Carries the `subscription` JSON so the app can retry the delete by
21081
+ * `endpoint` later; if it never does, the push service answers the next send
21082
+ * to that endpoint with `404`/`410`, which is the backend's cue to drop the row.
21083
+ */
21084
+ export declare class WebPushUnsubscribeError extends Error {
21085
+ /** The subscription whose backend delete failed. */
21086
+ readonly subscription: PushSubscriptionJSON;
21087
+ /** Whether `PushSubscription.unsubscribe()` reported success. */
21088
+ readonly browserUnsubscribed: boolean;
21089
+ /**
21090
+ * @param subscription - The subscription whose backend delete failed.
21091
+ * @param browserUnsubscribed - What `PushSubscription.unsubscribe()` returned.
21092
+ * @param cause - What `onUnsubscribe` threw.
21093
+ */
21094
+ constructor(subscription: PushSubscriptionJSON, browserUnsubscribed: boolean, cause: unknown);
21095
+ }
21096
+
19978
21097
  export declare class WebPushUnsupportedError extends Error {
19979
21098
  constructor();
19980
21099
  }