@athenaintel/react 0.26.6 → 0.27.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.
@@ -1,4 +1,5 @@
1
1
  import type { AthenaTokenRenewer } from '../auth/credential-source';
2
+ import { AthenaSdkError } from '../diagnostics/errors';
2
3
  export interface ParentBridgeState {
3
4
  /** PropelAuth access token from the parent window. */
4
5
  token: string | null;
@@ -25,6 +26,19 @@ export interface ParentBridgeState {
25
26
  hostAnswered: boolean;
26
27
  /** True once config has been received from the parent (or timeout fired). */
27
28
  ready: boolean;
29
+ /**
30
+ * Set when the Marathon mint (`/_athena/auth`) gave up. It gives up when it
31
+ * gets a refusal (401, 403, or any 4xx other than 404), a body it cannot
32
+ * read, or a transient failure (5xx, 408, 429, a network error, a timeout)
33
+ * on every attempt of one bounded retry cycle. `code` is
34
+ * `auth_bridge_failed`, or `auth_bridge_timeout` when the last attempt
35
+ * timed out. After that the bridge does not mint again in the background. A
36
+ * forced renewal (`renewToken({ force: true })`) starts one new bounded
37
+ * cycle. A token that an earlier mint handed out is kept (the server still
38
+ * validates it), and the bridge never puts an empty token in its place. The
39
+ * next successful mint clears it. Absent or `null` otherwise.
40
+ */
41
+ error?: AthenaSdkError | null;
28
42
  }
29
43
  /**
30
44
  * {@link ParentBridgeState} plus the bridge's token renewal: `force` (the
@@ -57,9 +71,65 @@ export interface ParentBridgeOptions {
57
71
  * Useful for private-cloud or custom-domain deployments.
58
72
  */
59
73
  trustedOrigins?: string[];
74
+ /** @internal Overrides {@link DEFAULT_MINT_RETRY_POLICY}; used by tests. */
75
+ mintRetryPolicy?: MintRetryPolicy;
76
+ }
77
+ /**
78
+ * How one Marathon mint cycle retries. A cycle is the initial acquisition, a
79
+ * scheduled refresh, or a forced renewal. It sends at most `maxAttempts`
80
+ * requests. It retries only a transient failure (5xx, 408, 429, a network
81
+ * error, a timeout), after a capped exponential backoff with jitter, and then
82
+ * it gives up. Nothing retries in the background after a cycle gave up.
83
+ */
84
+ export interface MintRetryPolicy {
85
+ /** Requests per cycle, the first one included. */
86
+ maxAttempts: number;
87
+ /** Ceiling of the delay before the first retry; it doubles for each retry. */
88
+ baseDelayMs: number;
89
+ /** Largest delay before any retry. */
90
+ maxDelayMs: number;
91
+ /** How long one request can take before it is aborted and counted as a timeout. */
92
+ timeoutMs: number;
93
+ /** Source of jitter in `[0, 1)`. */
94
+ random: () => number;
60
95
  }
96
+ export declare const DEFAULT_MINT_RETRY_POLICY: MintRetryPolicy;
61
97
  export declare const getTokenRefreshDelay: (expiresAt: unknown, nowMs: number) => number;
62
- export declare const getAuthRetryDelay: (retryAttempt: number) => number;
98
+ /**
99
+ * The delay before retry number `retry` of a mint cycle (1 is the first
100
+ * retry). The ceiling is `min(maxDelayMs, baseDelayMs * 2^(retry - 1))`, and
101
+ * equal jitter puts the wait between half of that ceiling and all of it, so
102
+ * viewers whose mint failed at the same time do not retry at the same time.
103
+ */
104
+ export declare function getMintRetryDelay({ retry, policy, }: {
105
+ retry: number;
106
+ policy?: Pick<MintRetryPolicy, 'baseDelayMs' | 'maxDelayMs' | 'random'>;
107
+ }): number;
108
+ /** A mint status worth another attempt: the host, or something in front of it, is briefly unwell. */
109
+ export declare function isRetryableMintStatus(status: number): boolean;
110
+ /** A failed `/_athena/auth` request that is worth another attempt. */
111
+ interface TransientMintFailure {
112
+ kind: 'transient';
113
+ status: number | null;
114
+ reason: 'status' | 'network' | 'timeout';
115
+ hostError: string | null;
116
+ cause?: unknown;
117
+ }
118
+ /** A failed `/_athena/auth` request that ends its cycle at once. */
119
+ interface RefusedMintFailure {
120
+ kind: 'refused';
121
+ status: number;
122
+ /** `status`: a 4xx other than 404, 408 and 429. `invalid_body`: a 2xx whose body is not a JSON object. */
123
+ reason: 'status' | 'invalid_body';
124
+ hostError: string | null;
125
+ }
126
+ export type MintFailure = TransientMintFailure | RefusedMintFailure;
127
+ /** The typed error that a mint cycle reports when it gives up. */
128
+ export declare function mintFailureError({ failure, attempts, phase, }: {
129
+ failure: MintFailure;
130
+ attempts: number;
131
+ phase: 'initial' | 'renewal';
132
+ }): AthenaSdkError;
63
133
  /**
64
134
  * Listens for auth tokens AND environment config from the host environment.
65
135
  *
@@ -75,12 +145,16 @@ export declare const getAuthRetryDelay: (retryAttempt: number) => number;
75
145
  * returns `{ token, statewireSyncUrl, backendBaseUrl, appUrl }` — the
76
146
  * statewire mount, the bare Agora base and the app origin. A 404 means
77
147
  * we're not on a Marathon origin — the hook falls through with empty
78
- * state.
148
+ * state. Any other failure fails closed: one bounded retry cycle
149
+ * ({@link MintRetryPolicy}) for a transient failure, none for a refusal,
150
+ * and then `error` is set and `ready` flips with no token — never an
151
+ * empty or anonymous one.
79
152
  *
80
- * In both modes a timeout safety net flips `ready: true` so the app never
81
- * blocks indefinitely.
153
+ * In both modes the app never blocks indefinitely: the iframe bridge flips
154
+ * `ready: true` after a timeout, and a Marathon mint cycle ends after at most
155
+ * `maxAttempts` requests.
82
156
  */
83
- export declare function useParentBridge({ trustedOrigins, }?: ParentBridgeOptions): ParentBridge;
157
+ export declare function useParentBridge({ trustedOrigins, mintRetryPolicy, }?: ParentBridgeOptions): ParentBridge;
84
158
  /**
85
159
  * Legacy hook — returns just the auth token from the parent bridge.
86
160
  * Prefer useParentBridge() for new code.
@@ -1,5 +1,6 @@
1
1
  import { useMemo, useState, useRef, useEffect, useCallback } from "react";
2
2
  import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
3
+ import { AthenaSdkError } from "../diagnostics/errors.js";
3
4
  function isTrustedOrigin({
4
5
  origin,
5
6
  trustedOrigins
@@ -41,10 +42,16 @@ function readAuthMintUrls(data) {
41
42
  const BRIDGE_TIMEOUT_MS = 2e3;
42
43
  const PARENT_TOKEN_RENEWAL_TIMEOUT_MS = 5e3;
43
44
  const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
44
- const TOKEN_REFRESH_RETRY_BASE_MS = 60 * 1e3;
45
- const TOKEN_REFRESH_RETRY_MAX_MS = 5 * 60 * 1e3;
45
+ const TOKEN_REFRESH_MIN_DELAY_MS = 60 * 1e3;
46
46
  const DEFAULT_TOKEN_REFRESH_MS = 55 * 60 * 1e3;
47
47
  const EMPTY_TRUSTED_ORIGINS = [];
48
+ const DEFAULT_MINT_RETRY_POLICY = {
49
+ maxAttempts: 4,
50
+ baseDelayMs: 500,
51
+ maxDelayMs: 4e3,
52
+ timeoutMs: BRIDGE_TIMEOUT_MS,
53
+ random: Math.random
54
+ };
48
55
  const normalizeOrigin = (value) => {
49
56
  try {
50
57
  return new URL(value).origin;
@@ -60,14 +67,74 @@ const getTokenRefreshDelay = (expiresAt, nowMs) => {
60
67
  if (!Number.isFinite(expiresAtMs)) {
61
68
  return DEFAULT_TOKEN_REFRESH_MS;
62
69
  }
63
- return Math.max(TOKEN_REFRESH_RETRY_BASE_MS, expiresAtMs - nowMs - TOKEN_REFRESH_BUFFER_MS);
64
- };
65
- const getAuthRetryDelay = (retryAttempt) => {
66
- const safeAttempt = Number.isFinite(retryAttempt) ? Math.max(0, Math.floor(retryAttempt)) : 0;
67
- return Math.min(TOKEN_REFRESH_RETRY_MAX_MS, TOKEN_REFRESH_RETRY_BASE_MS * 2 ** safeAttempt);
70
+ return Math.max(TOKEN_REFRESH_MIN_DELAY_MS, expiresAtMs - nowMs - TOKEN_REFRESH_BUFFER_MS);
68
71
  };
72
+ function getMintRetryDelay({
73
+ retry,
74
+ policy = DEFAULT_MINT_RETRY_POLICY
75
+ }) {
76
+ const safeRetry = Number.isFinite(retry) ? Math.max(1, Math.floor(retry)) : 1;
77
+ const ceiling = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (safeRetry - 1));
78
+ const jitter = Math.min(1, Math.max(0, policy.random()));
79
+ return Math.round(ceiling / 2 + ceiling / 2 * jitter);
80
+ }
81
+ function isRetryableMintStatus(status) {
82
+ return status >= 500 || status === 408 || status === 429;
83
+ }
84
+ async function readHostError(resp) {
85
+ try {
86
+ const body = await resp.json();
87
+ if (body && typeof body === "object" && "error" in body) {
88
+ const value = body.error;
89
+ return typeof value === "string" ? value.slice(0, 100) : null;
90
+ }
91
+ } catch {
92
+ }
93
+ return null;
94
+ }
95
+ const MINT_RETRY_NOTE = "The SDK stopped retrying and does not connect without a credential. Remount the provider, or call renewToken({ force: true }), to try again.";
96
+ function mintFailureHint(failure) {
97
+ if (failure.kind === "transient") {
98
+ return `The host could not mint a viewer token. Check the Marathon and Agora logs. ${MINT_RETRY_NOTE}`;
99
+ }
100
+ if (failure.reason === "invalid_body") {
101
+ return `The host answered /_athena/auth without a JSON mint body, so this origin is not a Marathon bridge. Pass token, getToken or apiKey. ${MINT_RETRY_NOTE}`;
102
+ }
103
+ if (failure.status === 401) {
104
+ return `The host has no session for this viewer. Sign in again through the app URL. ${MINT_RETRY_NOTE}`;
105
+ }
106
+ if (failure.status === 403) {
107
+ return `The host does not give this viewer access to the app. ${MINT_RETRY_NOTE}`;
108
+ }
109
+ return `The host refused the mint request. ${MINT_RETRY_NOTE}`;
110
+ }
111
+ function mintFailureError({
112
+ failure,
113
+ attempts,
114
+ phase
115
+ }) {
116
+ const timedOut = failure.kind === "transient" && failure.reason === "timeout";
117
+ const answer = failure.status !== null ? `HTTP ${failure.status}` : timedOut ? "a timeout" : "a network error";
118
+ const message = failure.kind === "transient" ? `The host auth bridge (/_athena/auth) failed ${attempts} time${attempts === 1 ? "" : "s"} (last: ${answer}).` : failure.reason === "invalid_body" ? `The host auth bridge (/_athena/auth) answered ${answer} without a JSON mint body.` : `The host auth bridge (/_athena/auth) refused the mint (${answer}).`;
119
+ return new AthenaSdkError({
120
+ code: timedOut ? "auth_bridge_timeout" : "auth_bridge_failed",
121
+ message,
122
+ hint: mintFailureHint(failure),
123
+ ...failure.status !== null && { status: failure.status },
124
+ ...failure.hostError !== null && { detail: { code: failure.hostError } },
125
+ ...failure.kind === "transient" && failure.cause !== void 0 && { cause: failure.cause },
126
+ context: {
127
+ mode: "marathon",
128
+ phase,
129
+ attempts,
130
+ reason: failure.reason,
131
+ host_error: failure.hostError
132
+ }
133
+ });
134
+ }
69
135
  function useParentBridge({
70
- trustedOrigins = EMPTY_TRUSTED_ORIGINS
136
+ trustedOrigins = EMPTY_TRUSTED_ORIGINS,
137
+ mintRetryPolicy = DEFAULT_MINT_RETRY_POLICY
71
138
  } = {}) {
72
139
  const isInIframe = typeof window !== "undefined" && window.parent !== window;
73
140
  const runtimeTrustedOrigins = useMemo(() => {
@@ -87,8 +154,11 @@ function useParentBridge({
87
154
  token: null,
88
155
  ...EMPTY_BRIDGE_URLS,
89
156
  hostAnswered: false,
90
- ready: false
157
+ ready: false,
158
+ error: null
91
159
  });
160
+ const mintRetryPolicyRef = useRef(mintRetryPolicy);
161
+ mintRetryPolicyRef.current = mintRetryPolicy;
92
162
  const readySignalSent = useRef(false);
93
163
  const configReceived = useRef(false);
94
164
  const tokenRef = useRef(state.token);
@@ -179,147 +249,189 @@ function useParentBridge({
179
249
  let refreshTimer = null;
180
250
  let controller = null;
181
251
  let inFlight = null;
182
- let retryAttempt = 0;
252
+ let retryWait = null;
183
253
  const clearRefreshTimer = () => {
184
254
  if (refreshTimer) {
185
255
  clearTimeout(refreshTimer);
186
256
  refreshTimer = null;
187
257
  }
188
258
  };
189
- const markReady = () => {
190
- if (cancelled) return;
191
- setState((prev) => prev.ready ? prev : { ...prev, ready: true });
259
+ const endRetryWait = () => {
260
+ if (!retryWait) return;
261
+ clearTimeout(retryWait.timer);
262
+ const { resolve } = retryWait;
263
+ retryWait = null;
264
+ resolve();
192
265
  };
266
+ const waitBeforeRetry = (delayMs) => new Promise((resolve) => {
267
+ retryWait = { timer: setTimeout(endRetryWait, delayMs), resolve };
268
+ });
193
269
  const scheduleRefresh = (expiresAt) => {
194
270
  clearRefreshTimer();
195
- retryAttempt = 0;
196
271
  const delay = getTokenRefreshDelay(expiresAt, Date.now());
197
272
  refreshTimer = setTimeout(() => {
198
- void fetchAuth({ markReadyOnFailure: false });
199
- }, delay);
200
- };
201
- const scheduleRetry = () => {
202
- clearRefreshTimer();
203
- const delay = getAuthRetryDelay(retryAttempt);
204
- retryAttempt += 1;
205
- refreshTimer = setTimeout(() => {
206
- void fetchAuth({ markReadyOnFailure: false });
273
+ refreshTimer = null;
274
+ void fetchAuth({ initial: false });
207
275
  }, delay);
208
276
  };
209
- const mintAuth = async ({
210
- markReadyOnFailure
211
- }) => {
277
+ const mintOnce = async (timeoutMs) => {
212
278
  const requestController = new AbortController();
213
279
  controller = requestController;
214
- const isInitial = markReadyOnFailure;
215
- const rawEndSpan = isInitial ? athenaDiagnostics.span("auth.bridge", "auth.bridge.start", { mode: "marathon" }) : null;
216
- let spanEnded = false;
217
- const endBridgeSpan = rawEndSpan ? (name, options) => {
218
- if (spanEnded) return 0;
219
- spanEnded = true;
220
- return rawEndSpan(name, options);
221
- } : null;
280
+ let timedOut = false;
222
281
  const requestTimer = setTimeout(() => {
282
+ timedOut = true;
223
283
  requestController.abort();
224
- endBridgeSpan == null ? void 0 : endBridgeSpan("auth.bridge.timeout", {
225
- data: { mode: "marathon", timeout_ms: BRIDGE_TIMEOUT_MS }
226
- });
227
- if (markReadyOnFailure) {
228
- markReady();
229
- }
230
- }, BRIDGE_TIMEOUT_MS);
284
+ }, timeoutMs);
231
285
  try {
232
286
  const resp = await fetch("/_athena/auth", {
233
287
  credentials: "include",
234
288
  headers: { Accept: "application/json" },
235
289
  signal: requestController.signal
236
290
  });
237
- if (cancelled) return null;
291
+ if (cancelled) return { kind: "cancelled" };
292
+ if (resp.status === 404) return { kind: "absent", status: resp.status };
238
293
  if (!resp.ok) {
239
- if (resp.status === 404) {
240
- endBridgeSpan == null ? void 0 : endBridgeSpan("auth.bridge.absent", {
241
- data: { mode: "marathon", status: resp.status }
242
- });
243
- markReady();
244
- } else {
245
- const failure = {
246
- level: "warn",
247
- data: { mode: "marathon", status: resp.status }
248
- };
249
- if (endBridgeSpan) {
250
- endBridgeSpan("auth.bridge.error", failure);
251
- } else {
252
- athenaDiagnostics.emit("auth.token.refresh_failed", failure);
253
- }
254
- if (markReadyOnFailure) {
255
- markReady();
256
- }
257
- scheduleRetry();
294
+ const hostError = await readHostError(resp);
295
+ if (cancelled) return { kind: "cancelled" };
296
+ if (timedOut && isRetryableMintStatus(resp.status)) {
297
+ return { kind: "transient", status: null, reason: "timeout", hostError: null };
258
298
  }
259
- return null;
260
- }
261
- const data = await resp.json();
262
- if (cancelled) return null;
263
- if (endBridgeSpan) {
264
- endBridgeSpan("auth.bridge.ready", {
265
- data: { mode: "marathon", has_token: typeof data.token === "string" }
266
- });
267
- athenaDiagnostics.emit("auth.token.acquired", {
268
- level: "debug",
269
- data: { source: "marathon", expires_at: data.expires_at }
270
- });
271
- } else {
272
- athenaDiagnostics.emit("auth.token.refreshed", {
273
- level: "debug",
274
- data: { source: "marathon", expires_at: data.expires_at }
275
- });
299
+ return isRetryableMintStatus(resp.status) ? { kind: "transient", status: resp.status, reason: "status", hostError } : { kind: "refused", status: resp.status, reason: "status", hostError };
276
300
  }
277
- const token = typeof data.token === "string" ? data.token : null;
278
- setState({
279
- token,
280
- ...readAuthMintUrls(data),
281
- hostAnswered: true,
282
- ready: true
283
- });
284
- tokenRef.current = token;
285
- scheduleRefresh(data.expires_at);
286
- return token;
287
- } catch (error) {
288
- if (!cancelled && !(error instanceof DOMException && error.name === "AbortError")) {
289
- const failure = { level: "warn", data: { mode: "marathon" }, error };
290
- if (endBridgeSpan) {
291
- endBridgeSpan("auth.bridge.error", failure);
292
- } else {
293
- athenaDiagnostics.emit("auth.token.refresh_failed", failure);
301
+ let data;
302
+ try {
303
+ data = await resp.json();
304
+ } catch (error) {
305
+ if (cancelled) return { kind: "cancelled" };
306
+ if (timedOut) {
307
+ return { kind: "transient", status: null, reason: "timeout", hostError: null, cause: error };
294
308
  }
309
+ return { kind: "refused", status: resp.status, reason: "invalid_body", hostError: null };
295
310
  }
296
- if (markReadyOnFailure) {
297
- markReady();
311
+ if (cancelled) return { kind: "cancelled" };
312
+ const token = data && typeof data === "object" ? data.token : null;
313
+ if (typeof token !== "string" || token === "") {
314
+ return { kind: "refused", status: resp.status, reason: "invalid_body", hostError: null };
298
315
  }
299
- if (!cancelled) {
300
- scheduleRetry();
316
+ return { kind: "minted", data };
317
+ } catch (error) {
318
+ if (cancelled) return { kind: "cancelled" };
319
+ if (timedOut) {
320
+ return { kind: "transient", status: null, reason: "timeout", hostError: null, cause: error };
301
321
  }
302
- return null;
322
+ return { kind: "transient", status: null, reason: "network", hostError: null, cause: error };
303
323
  } finally {
304
324
  clearTimeout(requestTimer);
305
325
  if (controller === requestController) controller = null;
306
326
  }
307
327
  };
328
+ const runMintCycle = async ({ initial }) => {
329
+ const policy = mintRetryPolicyRef.current;
330
+ const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
331
+ const phase = initial ? "initial" : "renewal";
332
+ const endBridgeSpan = initial ? athenaDiagnostics.span("auth.bridge", "auth.bridge.start", { mode: "marathon" }) : null;
333
+ let attempts = 0;
334
+ let lastFailure = null;
335
+ while (attempts < maxAttempts) {
336
+ attempts += 1;
337
+ const outcome = await mintOnce(policy.timeoutMs);
338
+ if (cancelled || outcome.kind === "cancelled") return null;
339
+ if (outcome.kind === "absent") {
340
+ endBridgeSpan == null ? void 0 : endBridgeSpan("auth.bridge.absent", {
341
+ data: { mode: "marathon", status: outcome.status }
342
+ });
343
+ setState(
344
+ (prev) => prev.ready && prev.error == null ? prev : { ...prev, ready: true, error: null }
345
+ );
346
+ return null;
347
+ }
348
+ if (outcome.kind === "minted") {
349
+ const { data } = outcome;
350
+ if (endBridgeSpan) {
351
+ endBridgeSpan("auth.bridge.ready", {
352
+ data: { mode: "marathon", has_token: typeof data.token === "string", attempts }
353
+ });
354
+ athenaDiagnostics.emit("auth.token.acquired", {
355
+ level: "debug",
356
+ data: { source: "marathon", expires_at: data.expires_at }
357
+ });
358
+ } else {
359
+ athenaDiagnostics.emit("auth.token.refreshed", {
360
+ level: "debug",
361
+ data: { source: "marathon", expires_at: data.expires_at, attempts }
362
+ });
363
+ }
364
+ const token = typeof data.token === "string" ? data.token : null;
365
+ setState({
366
+ token,
367
+ ...readAuthMintUrls(data),
368
+ hostAnswered: true,
369
+ ready: true,
370
+ error: null
371
+ });
372
+ tokenRef.current = token;
373
+ scheduleRefresh(data.expires_at);
374
+ return token;
375
+ }
376
+ lastFailure = outcome;
377
+ if (outcome.kind === "refused" || attempts >= maxAttempts) break;
378
+ const delayMs = getMintRetryDelay({ retry: attempts, policy });
379
+ athenaDiagnostics.emit("auth.bridge.retry", {
380
+ level: "warn",
381
+ data: {
382
+ mode: "marathon",
383
+ phase,
384
+ status: outcome.status,
385
+ reason: outcome.reason,
386
+ host_error: outcome.hostError,
387
+ attempt: attempts,
388
+ max_attempts: maxAttempts,
389
+ delay_ms: delayMs
390
+ }
391
+ });
392
+ await waitBeforeRetry(delayMs);
393
+ if (cancelled) return null;
394
+ }
395
+ if (!lastFailure) return null;
396
+ const error = mintFailureError({ failure: lastFailure, attempts, phase });
397
+ const failureData = {
398
+ mode: "marathon",
399
+ phase,
400
+ status: lastFailure.status,
401
+ reason: lastFailure.reason,
402
+ attempts
403
+ };
404
+ if (endBridgeSpan) {
405
+ endBridgeSpan(
406
+ error.code === "auth_bridge_timeout" ? "auth.bridge.timeout" : "auth.bridge.error",
407
+ { data: failureData, error }
408
+ );
409
+ } else {
410
+ athenaDiagnostics.emit("auth.token.refresh_failed", {
411
+ level: "error",
412
+ data: failureData,
413
+ error
414
+ });
415
+ }
416
+ setState((prev) => ({ ...prev, error, ready: true }));
417
+ return null;
418
+ };
308
419
  const fetchAuth = (options) => {
309
- inFlight ?? (inFlight = mintAuth(options).finally(() => {
420
+ inFlight ?? (inFlight = runMintCycle(options).finally(() => {
310
421
  inFlight = null;
311
422
  }));
312
423
  return inFlight;
313
424
  };
314
425
  renewerRef.current = ({ force }) => {
315
426
  if (!force) return Promise.resolve(tokenRef.current);
316
- return fetchAuth({ markReadyOnFailure: false }).then((token) => token ?? tokenRef.current);
427
+ return fetchAuth({ initial: false }).then((token) => token ?? tokenRef.current);
317
428
  };
318
- void fetchAuth({ markReadyOnFailure: true });
429
+ void fetchAuth({ initial: true });
319
430
  return () => {
320
431
  cancelled = true;
321
432
  controller == null ? void 0 : controller.abort();
322
433
  clearRefreshTimer();
434
+ endRetryWait();
323
435
  renewerRef.current = null;
324
436
  };
325
437
  }, [isInIframe]);
@@ -336,9 +448,12 @@ function useParentAuth() {
336
448
  return useParentBridge().token;
337
449
  }
338
450
  export {
339
- getAuthRetryDelay,
451
+ DEFAULT_MINT_RETRY_POLICY,
452
+ getMintRetryDelay,
340
453
  getTokenRefreshDelay,
454
+ isRetryableMintStatus,
341
455
  mergeBridgeConfigMessage,
456
+ mintFailureError,
342
457
  readAuthMintUrls,
343
458
  useParentAuth,
344
459
  useParentBridge