tempest-react-sdk 0.79.0 → 0.80.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.
@@ -768,6 +768,20 @@ export declare interface ApiClient {
768
768
  */
769
769
  arrayBuffer(path: string, options?: RequestOptions): Promise<ArrayBuffer>;
770
770
  upload<T>(path: string, formData: FormData, method?: "POST" | "PUT" | "PATCH", options?: Omit<RequestOptions, "body" | "method">): Promise<T>;
771
+ /**
772
+ * The absolute URL a request to `path` would hit, without sending one.
773
+ *
774
+ * For the consumers that cannot go through `fetch` but must land on the
775
+ * same host: an `EventSource`, an `<img src>`, a download link. Rebuilding
776
+ * the URL by hand duplicates `baseURL` and `prefix`, and a forgotten prefix
777
+ * is a silent 404. No credential is attached: the URL is a string, and a
778
+ * token placed in it would end up in every proxy log.
779
+ *
780
+ * @param path - Path joined onto the client's base URL and prefix.
781
+ * @param params - Query params to append; `undefined` and `null` are skipped.
782
+ * @returns The absolute URL.
783
+ */
784
+ url(path: string, params?: RequestOptions["params"]): string;
771
785
  }
772
786
 
773
787
  export declare interface ApiClientConfig {
@@ -2417,7 +2431,10 @@ export { BrowserRouter }
2417
2431
  *
2418
2432
  * A path that is itself an absolute URL wins over all of this, which is how a
2419
2433
  * client reaches a second host (a signed upload endpoint, a CDN) without a
2420
- * second client.
2434
+ * second client. Without `params` it is returned exactly as given: a signed
2435
+ * URL covers its query, and a round trip through `URL` re-encodes characters
2436
+ * outside the canonical form (`'` became `%27`, measured), which is a rewrite
2437
+ * of a string the server signed.
2421
2438
  *
2422
2439
  * @example
2423
2440
  * buildApiUrl("https://api.example.com", "/auth/login", { prefix: "/api" });
@@ -4220,11 +4237,15 @@ export declare function createDataProvider(client: ApiClient, options?: DataProv
4220
4237
  * `onMessage`. Pass `withCredentials: true` when the backend authenticates via
4221
4238
  * cookies. Call `close()` from the returned controller to tear down.
4222
4239
  *
4223
- * @param url - Full SSE endpoint URL.
4240
+ * When `url` is a function it is called before every connection, so a
4241
+ * reconnect presents a fresh credential instead of the one that expired. A
4242
+ * resolution still pending when `close()` or `reconnect()` runs is discarded.
4243
+ *
4244
+ * @param url - Full SSE endpoint URL, or a function returning it per connection.
4224
4245
  * @param options - Stream configuration and callbacks.
4225
4246
  * @returns A controller exposing `close`, `reconnect` and the current `status`.
4226
4247
  */
4227
- export declare function createEventStream<T = unknown>(url: string, options?: CreateEventStreamOptions<T>): EventStreamController;
4248
+ export declare function createEventStream<T = unknown>(url: EventStreamUrl, options?: CreateEventStreamOptions<T>): EventStreamController;
4228
4249
 
4229
4250
  export declare interface CreateEventStreamOptions<T> {
4230
4251
  /** Send cookies with the EventSource handshake. Default: false. */
@@ -4279,6 +4300,21 @@ export declare interface CreateEventStreamOptions<T> {
4279
4300
  * `isDevBuild()` to be able to answer, and this callback is the app's own.
4280
4301
  */
4281
4302
  onValidationError?: (issues: SchemaIssue[], raw: string) => void;
4303
+ /**
4304
+ * The URL function threw or rejected, so no connection was attempted.
4305
+ *
4306
+ * `onError` cannot carry this: it receives the `EventSource` `Event`, and a
4307
+ * URL that never resolved never had an `EventSource`.
4308
+ */
4309
+ onUrlError?: (error: unknown) => void;
4310
+ /**
4311
+ * Whether a URL-resolution failure is worth another attempt. Default: always.
4312
+ *
4313
+ * Return `false` for failures no backoff can fix — a `401` or a `403` when
4314
+ * fetching a ticket is a session decision, not a network blip — and the stream
4315
+ * stops at `status: "error"` instead of spending `maxRetries` on it.
4316
+ */
4317
+ shouldRetryUrl?: (error: unknown) => boolean;
4282
4318
  onOpen?: () => void;
4283
4319
  onMessage?: (message: EventStreamMessage<T>) => void;
4284
4320
  onError?: (error: Event) => void;
@@ -5478,6 +5514,35 @@ export declare interface CreateThemeOptions {
5478
5514
  inverseSelector?: string;
5479
5515
  }
5480
5516
 
5517
+ /**
5518
+ * Open the ticket-authenticated stream of the `tempest-fastapi-sdk` SSE router.
5519
+ *
5520
+ * `POST <prefix>/ticket` through the authenticated client, then
5521
+ * `GET <prefix>/stream?ticket=…` — and again for every reconnect, because the
5522
+ * ticket only opens streams for `expires_in` seconds and a reconnect after that
5523
+ * with the old one is a `401`. A ticket request refused with a status no
5524
+ * backoff fixes (`401`, `403 ACCOUNT_INACTIVE`) stops the stream at
5525
+ * `status: "error"` and reaches `onUrlError`; pass `shouldRetryUrl` to replace
5526
+ * {@link isRetriableTicketError}.
5527
+ *
5528
+ * The access token never enters the URL: it travels as the client's header (or
5529
+ * cookie) on the ticket request, and the ticket in the URL opens that stream
5530
+ * and nothing else.
5531
+ *
5532
+ * @example
5533
+ * const stream = createTicketEventStream<Notification>({
5534
+ * client: api,
5535
+ * onMessage: ({ data }) => addNotification(data),
5536
+ * onUrlError: (error) => toast.error(describeApiError(error)),
5537
+ * });
5538
+ *
5539
+ * @param options - Ticket source plus every {@link createEventStream} option.
5540
+ * @returns The stream controller.
5541
+ */
5542
+ export declare function createTicketEventStream<T = unknown>(options: CreateTicketEventStreamOptions<T>): EventStreamController;
5543
+
5544
+ export declare type CreateTicketEventStreamOptions<T> = TicketEventStreamSource & CreateEventStreamOptions<T>;
5545
+
5481
5546
  /**
5482
5547
  * Record a video stream — a camera, a screen share, or a `canvas.captureStream()`.
5483
5548
  *
@@ -7326,6 +7391,19 @@ export declare interface EventStreamMessage<T> {
7326
7391
 
7327
7392
  export declare type EventStreamStatus = "idle" | "connecting" | "open" | "closed" | "error";
7328
7393
 
7394
+ /**
7395
+ * Where the stream connects: a fixed URL, or a function resolved again before
7396
+ * every connection, the first one and each reconnect.
7397
+ *
7398
+ * The function form exists for short-lived credentials carried in the URL,
7399
+ * such as the `?ticket=` of the `tempest-fastapi-sdk` SSE router. `EventSource`
7400
+ * cannot send headers, so the credential has to ride in the query, and one that
7401
+ * expires makes a fixed URL a trap: the reconnect after the expiry presents the
7402
+ * same dead credential, gets a `401`, and burns every retry on it while the user
7403
+ * is still signed in.
7404
+ */
7405
+ export declare type EventStreamUrl = string | (() => string | Promise<string>);
7406
+
7329
7407
  /**
7330
7408
  * Finish a `tempest-fastapi-sdk` social login from the SPA's callback route.
7331
7409
  *
@@ -9478,6 +9556,21 @@ export declare function isRejectionCloseCode(code: number): boolean;
9478
9556
  */
9479
9557
  export declare function isRetriableStatus(status: number): boolean;
9480
9558
 
9559
+ /**
9560
+ * Whether a failure to obtain a ticket is worth another attempt.
9561
+ *
9562
+ * Only a failure the backoff can outlast is: no response at all, a timeout, a
9563
+ * rate limit, a `5xx`. A `401` reached the caller after the client already
9564
+ * tried its refresh, and a `403` (`ACCOUNT_INACTIVE`) is the backend saying no;
9565
+ * both are session decisions, and retrying them only delays the error state the
9566
+ * app has to show. Anything that is not an API error is a contract mismatch,
9567
+ * which a retry cannot fix either.
9568
+ *
9569
+ * @param error - What the ticket request threw.
9570
+ * @returns Whether to schedule a reconnect.
9571
+ */
9572
+ export declare function isRetriableTicketError(error: unknown): boolean;
9573
+
9481
9574
  export { isRouteErrorResponse }
9482
9575
 
9483
9576
  /** Whether `getDisplayMedia` is reachable at all. */
@@ -12926,6 +13019,22 @@ export declare type PreloadableLazy<T extends AnyComponent> = ReturnType<typeof
12926
13019
  }>;
12927
13020
  };
12928
13021
 
13022
+ /**
13023
+ * A pre-signed `POST` form, as `tempest-fastapi-sdk` >= 0.331.0 hands it out
13024
+ * (`presigned_post_policy` / `SubjectObjectStorage.presign_post`).
13025
+ *
13026
+ * Snake case on purpose: this is the wire shape, so it parses straight from the
13027
+ * response of the app's own route.
13028
+ */
13029
+ export declare interface PresignedPost {
13030
+ /** Where the form is posted: the bucket on S3/MinIO, or the API itself on the local backend. */
13031
+ url: string;
13032
+ /** Form fields to send before the file, in this order — `key`, `policy`, signature and the rest. */
13033
+ fields: Record<string, string>;
13034
+ /** Largest file the policy accepts, in bytes. */
13035
+ max_bytes: number;
13036
+ }
13037
+
12929
13038
  /** A model whose cached size should appear in the report. */
12930
13039
  export declare interface ProfiledModel {
12931
13040
  /** Label for the report row, e.g. `"detector"`. */
@@ -15829,6 +15938,14 @@ export declare interface SpinnerProps {
15829
15938
 
15830
15939
  export declare type SpinnerSize = "xs" | "sm" | "md" | "lg" | "xl";
15831
15940
 
15941
+ /** Body of `POST <prefix>/ticket` on the `tempest-fastapi-sdk` SSE router. */
15942
+ export declare interface SseTicketResponse {
15943
+ /** Signed stream ticket, presented as a query param when opening the stream. */
15944
+ ticket: string;
15945
+ /** Seconds the ticket is accepted for opening a stream. */
15946
+ expires_in: number;
15947
+ }
15948
+
15832
15949
  /** Flex-based vertical or horizontal stack with a numeric `gap`. */
15833
15950
  export declare function Stack({ direction, gap, align, justify, wrap, className, style, children, ...props }: StackProps): JSX.Element;
15834
15951
 
@@ -16581,6 +16698,20 @@ export declare function throttle<A extends unknown[]>(fn: (...args: A) => void,
16581
16698
  cancel: () => void;
16582
16699
  };
16583
16700
 
16701
+ /** Where the ticket comes from and how the stream URL is built. */
16702
+ export declare interface TicketEventStreamSource {
16703
+ /**
16704
+ * Authenticated client the ticket is requested through, so it carries the
16705
+ * `Authorization` header or the session cookie, the CSRF header, and the
16706
+ * 401 refresh-and-replay that a hand-rolled `fetch` would lose.
16707
+ */
16708
+ client: Pick<ApiClient, "post" | "url">;
16709
+ /** Router prefix, relative to the client's base URL. Default: `"/sse"`. */
16710
+ prefix?: string;
16711
+ /** Query param the stream reads the ticket from. Default: `"ticket"`. */
16712
+ ticketParam?: string;
16713
+ }
16714
+
16584
16715
  /**
16585
16716
  * Vertical event timeline — activity feeds, order trackers, audit logs.
16586
16717
  * Each entry has a marker (color or custom icon), title, optional
@@ -17363,6 +17494,48 @@ export declare interface UpdateVariables {
17363
17494
  */
17364
17495
  export declare function uploadFingerprint(endpoint: string, file: Blob | File): string;
17365
17496
 
17497
+ /**
17498
+ * Upload a file straight to storage through a pre-signed `POST` form, with the
17499
+ * size ceiling the policy enforces.
17500
+ *
17501
+ * The form is built the way S3 requires: every `post.fields` entry in order,
17502
+ * the file last in the `file` part — S3 ignores whatever follows the file.
17503
+ *
17504
+ * The request carries **no** credential and no custom header: no
17505
+ * `Authorization`, no CSRF token, no `X-Request-ID`, no cookies. `post.url` is
17506
+ * usually the storage host, outside the API's origin, and the signature in
17507
+ * `fields` is the whole authorization. Without custom headers the request is a
17508
+ * CORS "simple request", so it does not depend on the bucket's CORS rules
17509
+ * allowing a preflight.
17510
+ *
17511
+ * A file larger than `post.max_bytes` is refused before anything is sent, with
17512
+ * the same `413 FILE_TOO_LARGE` the server would answer. Server errors are
17513
+ * normalized into one `TempestApiError` whichever backend answered: the S3 XML
17514
+ * (`EntityTooLarge` → `FILE_TOO_LARGE`, `EntityTooSmall` → `FILE_TOO_SMALL`,
17515
+ * `AccessDenied` → `SIGNED_URL_INVALID`, or `SIGNED_URL_EXPIRED` when the policy
17516
+ * expired) or the local backend's JSON envelope, which already uses those codes.
17517
+ *
17518
+ * @example
17519
+ * const post = await api.post<PresignedPost>("/avatars/upload-form");
17520
+ * await uploadPresignedPost(post, file, { onProgress: (p) => setProgress(p.fraction) });
17521
+ *
17522
+ * @param post - The form the API handed out.
17523
+ * @param file - The file or blob to upload.
17524
+ * @param options - Progress callback, abort signal and file name.
17525
+ * @returns Resolves when storage accepted the file (`204` on S3).
17526
+ * @throws TempestApiError On a refused upload, with the normalized `code`.
17527
+ */
17528
+ export declare function uploadPresignedPost(post: PresignedPost, file: Blob, options?: UploadPresignedPostOptions): Promise<void>;
17529
+
17530
+ export declare interface UploadPresignedPostOptions {
17531
+ /** Called on every `progress` event of the upload. */
17532
+ onProgress?: (event: UploadProgressEvent) => void;
17533
+ /** Abort the upload; rejects with an `AbortError` `DOMException`. */
17534
+ signal?: AbortSignal;
17535
+ /** File name sent with the `file` part. Defaults to `file.name` for a `File`. */
17536
+ fileName?: string;
17537
+ }
17538
+
17366
17539
  export declare interface UploadProgressEvent {
17367
17540
  /** Bytes already uploaded. */
17368
17541
  loaded: number;
@@ -18144,6 +18317,11 @@ export declare function useEventListener<K extends keyof HTMLElementEventMap>(ev
18144
18317
  * tied to the component (and the `url`/`enabled` dependencies); the stream
18145
18318
  * closes on unmount.
18146
18319
  *
18320
+ * A string `url` reopens the stream when it changes. A function `url` does not:
18321
+ * an inline arrow is a new function every render, and keying on its identity
18322
+ * would reconnect on every render. The latest function is the one called on the
18323
+ * next connection, so call `reconnect()` when what it resolves to must change now.
18324
+ *
18147
18325
  * Pass `schema` to have every frame validated before it reaches `onMessage`,
18148
18326
  * instead of trusting the type argument: without it `data` is announced as `T`
18149
18327
  * on the strength of the generic alone, which is a promise about the server
@@ -18167,7 +18345,7 @@ export declare function useEventListener<K extends keyof HTMLElementEventMap>(ev
18167
18345
  * onMessage: ({ data }) => addNotification(data),
18168
18346
  * });
18169
18347
  */
18170
- export declare function useEventStream<T = unknown>(url: string, options?: UseEventStreamOptions<T>): UseEventStreamResult<T>;
18348
+ export declare function useEventStream<T = unknown>(url: EventStreamUrl, options?: UseEventStreamOptions<T>): UseEventStreamResult<T>;
18171
18349
 
18172
18350
  export declare interface UseEventStreamOptions<T> extends Omit<CreateEventStreamOptions<T>, "onStatusChange"> {
18173
18351
  /** When false, the stream is not opened. Useful for "wait for auth". Default: true. */
@@ -20186,6 +20364,30 @@ export declare function useTheme(): ThemeContextValue;
20186
20364
  */
20187
20365
  export declare function useThrottle<T>(value: T, delay: number): T;
20188
20366
 
20367
+ /**
20368
+ * React form of {@link createTicketEventStream}: same ticket per connection,
20369
+ * lifecycle tied to the component like {@link useEventStream}.
20370
+ *
20371
+ * Changing `client`, `prefix` or `ticketParam` does not drop an open stream:
20372
+ * the next connection uses the new values, and `reconnect()` applies them now.
20373
+ * Reopening on identity would reconnect on every render for a client built
20374
+ * inline.
20375
+ *
20376
+ * @example
20377
+ * const { status } = useTicketEventStream<Notification>({
20378
+ * client: api,
20379
+ * enabled: isAuthenticated,
20380
+ * schema: notificationSchema,
20381
+ * onMessage: ({ data }) => addNotification(data),
20382
+ * });
20383
+ *
20384
+ * @param options - Ticket source plus every {@link useEventStream} option.
20385
+ * @returns Status, last message and `reconnect`.
20386
+ */
20387
+ export declare function useTicketEventStream<T = unknown>(options: UseTicketEventStreamOptions<T>): UseEventStreamResult<T>;
20388
+
20389
+ export declare type UseTicketEventStreamOptions<T> = TicketEventStreamSource & UseEventStreamOptions<T>;
20390
+
20189
20391
  /**
20190
20392
  * Run a callback after `delay` ms. Pass `null` to disable. Resets when
20191
20393
  * `delay` changes; the callback ref always points at the latest closure.