@athenaintel/react 0.26.6 → 0.28.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 (75) hide show
  1. package/README.md +32 -1
  2. package/dist/chat/StatewireApprovalCard.cjs +19 -0
  3. package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
  4. package/dist/chat/StatewireApprovalCard.js +19 -0
  5. package/dist/chat/StatewireApprovalCard.js.map +1 -1
  6. package/dist/chat/message-time.cjs.map +1 -1
  7. package/dist/chat/message-time.d.ts +4 -3
  8. package/dist/chat/message-time.js.map +1 -1
  9. package/dist/composer/QuoteContext.cjs +13 -1
  10. package/dist/composer/QuoteContext.cjs.map +1 -1
  11. package/dist/composer/QuoteContext.d.ts +11 -1
  12. package/dist/composer/QuoteContext.js +13 -1
  13. package/dist/composer/QuoteContext.js.map +1 -1
  14. package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
  15. package/dist/diagnostics/athena-diagnostics.d.ts +1 -1
  16. package/dist/diagnostics/athena-diagnostics.js.map +1 -1
  17. package/dist/diagnostics/errors.cjs +11 -2
  18. package/dist/diagnostics/errors.cjs.map +1 -1
  19. package/dist/diagnostics/errors.d.ts +11 -2
  20. package/dist/diagnostics/errors.js +11 -2
  21. package/dist/diagnostics/errors.js.map +1 -1
  22. package/dist/lib/athena-urls.cjs +1 -1
  23. package/dist/lib/athena-urls.cjs.map +1 -1
  24. package/dist/lib/athena-urls.js +1 -1
  25. package/dist/lib/athena-urls.js.map +1 -1
  26. package/dist/lib/posthog/mask-sensitive-input.cjs +34 -0
  27. package/dist/lib/posthog/mask-sensitive-input.cjs.map +1 -0
  28. package/dist/lib/posthog/mask-sensitive-input.d.ts +9 -0
  29. package/dist/lib/posthog/mask-sensitive-input.js +34 -0
  30. package/dist/lib/posthog/mask-sensitive-input.js.map +1 -0
  31. package/dist/packages/agent-runtime/src/conversation-index.cjs +2 -1
  32. package/dist/packages/agent-runtime/src/conversation-index.cjs.map +1 -1
  33. package/dist/packages/agent-runtime/src/conversation-index.js +2 -1
  34. package/dist/packages/agent-runtime/src/conversation-index.js.map +1 -1
  35. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +2 -0
  36. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  37. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +2 -0
  38. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  39. package/dist/packages/agent-runtime/src/interrupts.cjs +26 -8
  40. package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
  41. package/dist/packages/agent-runtime/src/interrupts.js +26 -8
  42. package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
  43. package/dist/packages/agent-runtime/src/messages.cjs +70 -14
  44. package/dist/packages/agent-runtime/src/messages.cjs.map +1 -1
  45. package/dist/packages/agent-runtime/src/messages.js +71 -15
  46. package/dist/packages/agent-runtime/src/messages.js.map +1 -1
  47. package/dist/packages/agent-runtime/src/tool-metadata.cjs +75 -0
  48. package/dist/packages/agent-runtime/src/tool-metadata.cjs.map +1 -1
  49. package/dist/packages/agent-runtime/src/tool-metadata.js +75 -0
  50. package/dist/packages/agent-runtime/src/tool-metadata.js.map +1 -1
  51. package/dist/packages/agent-runtime/src/tool-result.cjs +19 -4
  52. package/dist/packages/agent-runtime/src/tool-result.cjs.map +1 -1
  53. package/dist/packages/agent-runtime/src/tool-result.js +19 -4
  54. package/dist/packages/agent-runtime/src/tool-result.js.map +1 -1
  55. package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs +23 -4
  56. package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
  57. package/dist/packages/agent-runtime/src/use-deep-agent-thread.js +24 -5
  58. package/dist/packages/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
  59. package/dist/packages/react/package.json.cjs +1 -1
  60. package/dist/packages/react/package.json.js +1 -1
  61. package/dist/provider/AthenaProvider.cjs +3 -0
  62. package/dist/provider/AthenaProvider.cjs.map +1 -1
  63. package/dist/provider/AthenaProvider.js +3 -0
  64. package/dist/provider/AthenaProvider.js.map +1 -1
  65. package/dist/provider/PostHogProvider.cjs +3 -1
  66. package/dist/provider/PostHogProvider.cjs.map +1 -1
  67. package/dist/provider/PostHogProvider.js +3 -1
  68. package/dist/provider/PostHogProvider.js.map +1 -1
  69. package/dist/runtime/auth.cjs +307 -108
  70. package/dist/runtime/auth.cjs.map +1 -1
  71. package/dist/runtime/auth.d.ts +151 -10
  72. package/dist/runtime/auth.js +307 -108
  73. package/dist/runtime/auth.js.map +1 -1
  74. package/dist/styles.css +1 -1
  75. package/package.json +1 -1
@@ -1,6 +1,17 @@
1
1
  import { useMemo, useState, useRef, useEffect, useCallback } from "react";
2
2
  import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
3
- function isTrustedOrigin({
3
+ import { AthenaSdkError } from "../diagnostics/errors.js";
4
+ const CUSTOMER_APP_HOST = /^.+\.(?:(?:[a-z0-9-]+-)?app(?:\.[a-z0-9-]+)*|computer(?:-[a-z0-9-]+)?\.stg|code-sandpack(?:\.[a-z0-9-]+)*)\.athenaintel\.com$/i;
5
+ function isCustomerAppHost(hostname) {
6
+ return CUSTOMER_APP_HOST.test(hostname);
7
+ }
8
+ function isLoopbackHost(hostname) {
9
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
10
+ }
11
+ function isAthenaHost(hostname) {
12
+ return hostname === "athenaintel.com" || hostname.endsWith(".athenaintel.com");
13
+ }
14
+ function isTrustedParentOrigin({
4
15
  origin,
5
16
  trustedOrigins
6
17
  }) {
@@ -10,11 +21,37 @@ function isTrustedOrigin({
10
21
  return true;
11
22
  }
12
23
  const { hostname } = new URL(normalizedOrigin);
13
- return hostname === "athenaintel.com" || hostname.endsWith(".athenaintel.com") || hostname === "localhost";
24
+ if (isCustomerAppHost(hostname)) {
25
+ return false;
26
+ }
27
+ return isAthenaHost(hostname) || hostname === "localhost";
14
28
  } catch {
15
29
  return false;
16
30
  }
17
31
  }
32
+ function refuseBridgeUrl({
33
+ url,
34
+ parentOrigin
35
+ }) {
36
+ let parsed;
37
+ let parentHostname;
38
+ try {
39
+ parsed = new URL(url);
40
+ parentHostname = new URL(parentOrigin).hostname;
41
+ } catch {
42
+ return "invalid";
43
+ }
44
+ if (parsed.username || parsed.password) return "invalid";
45
+ const { hostname } = parsed;
46
+ const loopbackPair = isLoopbackHost(hostname) && isLoopbackHost(parentHostname);
47
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopbackPair)) {
48
+ return "insecure";
49
+ }
50
+ if (isCustomerAppHost(hostname)) return "customer_app_host";
51
+ if (loopbackPair || isAthenaHost(hostname)) return null;
52
+ if (isAthenaHost(parentHostname) || isLoopbackHost(parentHostname)) return "other_domain";
53
+ return hostname === parentHostname || hostname.endsWith(`.${parentHostname}`) ? null : "other_domain";
54
+ }
18
55
  const BRIDGE_URL_FIELDS = ["statewireSyncUrl", "backendBaseUrl", "appUrl"];
19
56
  const EMPTY_BRIDGE_URLS = {
20
57
  statewireSyncUrl: null,
@@ -35,16 +72,51 @@ function mergeBridgeConfigMessage(previous, payload) {
35
72
  }
36
73
  return next;
37
74
  }
75
+ function acceptBridgeConfigMessage({
76
+ previous,
77
+ payload,
78
+ parentOrigin,
79
+ locked = false
80
+ }) {
81
+ const urls = {
82
+ statewireSyncUrl: previous.statewireSyncUrl,
83
+ backendBaseUrl: previous.backendBaseUrl,
84
+ appUrl: previous.appUrl
85
+ };
86
+ const rejected = [];
87
+ for (const field of BRIDGE_URL_FIELDS) {
88
+ const value = payload[field];
89
+ if (typeof value !== "string") continue;
90
+ const held = previous[field];
91
+ if (locked || held !== null) {
92
+ if (held !== value) rejected.push({ field, reason: "changed" });
93
+ continue;
94
+ }
95
+ const refusal = refuseBridgeUrl({ url: value, parentOrigin });
96
+ if (refusal !== null) {
97
+ rejected.push({ field, reason: refusal });
98
+ continue;
99
+ }
100
+ urls[field] = value;
101
+ }
102
+ return { urls, rejected };
103
+ }
38
104
  function readAuthMintUrls(data) {
39
105
  return mergeBridgeConfigMessage(EMPTY_BRIDGE_URLS, data);
40
106
  }
41
107
  const BRIDGE_TIMEOUT_MS = 2e3;
42
108
  const PARENT_TOKEN_RENEWAL_TIMEOUT_MS = 5e3;
43
109
  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;
110
+ const TOKEN_REFRESH_MIN_DELAY_MS = 60 * 1e3;
46
111
  const DEFAULT_TOKEN_REFRESH_MS = 55 * 60 * 1e3;
47
112
  const EMPTY_TRUSTED_ORIGINS = [];
113
+ const DEFAULT_MINT_RETRY_POLICY = {
114
+ maxAttempts: 4,
115
+ baseDelayMs: 500,
116
+ maxDelayMs: 4e3,
117
+ timeoutMs: BRIDGE_TIMEOUT_MS,
118
+ random: Math.random
119
+ };
48
120
  const normalizeOrigin = (value) => {
49
121
  try {
50
122
  return new URL(value).origin;
@@ -60,14 +132,74 @@ const getTokenRefreshDelay = (expiresAt, nowMs) => {
60
132
  if (!Number.isFinite(expiresAtMs)) {
61
133
  return DEFAULT_TOKEN_REFRESH_MS;
62
134
  }
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);
135
+ return Math.max(TOKEN_REFRESH_MIN_DELAY_MS, expiresAtMs - nowMs - TOKEN_REFRESH_BUFFER_MS);
68
136
  };
137
+ function getMintRetryDelay({
138
+ retry,
139
+ policy = DEFAULT_MINT_RETRY_POLICY
140
+ }) {
141
+ const safeRetry = Number.isFinite(retry) ? Math.max(1, Math.floor(retry)) : 1;
142
+ const ceiling = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (safeRetry - 1));
143
+ const jitter = Math.min(1, Math.max(0, policy.random()));
144
+ return Math.round(ceiling / 2 + ceiling / 2 * jitter);
145
+ }
146
+ function isRetryableMintStatus(status) {
147
+ return status >= 500 || status === 408 || status === 429;
148
+ }
149
+ async function readHostError(resp) {
150
+ try {
151
+ const body = await resp.json();
152
+ if (body && typeof body === "object" && "error" in body) {
153
+ const value = body.error;
154
+ return typeof value === "string" ? value.slice(0, 100) : null;
155
+ }
156
+ } catch {
157
+ }
158
+ return null;
159
+ }
160
+ 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.";
161
+ function mintFailureHint(failure) {
162
+ if (failure.kind === "transient") {
163
+ return `The host could not mint a viewer token. Check the Marathon and Agora logs. ${MINT_RETRY_NOTE}`;
164
+ }
165
+ if (failure.reason === "invalid_body") {
166
+ 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}`;
167
+ }
168
+ if (failure.status === 401) {
169
+ return `The host has no session for this viewer. Sign in again through the app URL. ${MINT_RETRY_NOTE}`;
170
+ }
171
+ if (failure.status === 403) {
172
+ return `The host does not give this viewer access to the app. ${MINT_RETRY_NOTE}`;
173
+ }
174
+ return `The host refused the mint request. ${MINT_RETRY_NOTE}`;
175
+ }
176
+ function mintFailureError({
177
+ failure,
178
+ attempts,
179
+ phase
180
+ }) {
181
+ const timedOut = failure.kind === "transient" && failure.reason === "timeout";
182
+ const answer = failure.status !== null ? `HTTP ${failure.status}` : timedOut ? "a timeout" : "a network error";
183
+ 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}).`;
184
+ return new AthenaSdkError({
185
+ code: timedOut ? "auth_bridge_timeout" : "auth_bridge_failed",
186
+ message,
187
+ hint: mintFailureHint(failure),
188
+ ...failure.status !== null && { status: failure.status },
189
+ ...failure.hostError !== null && { detail: { code: failure.hostError } },
190
+ ...failure.kind === "transient" && failure.cause !== void 0 && { cause: failure.cause },
191
+ context: {
192
+ mode: "marathon",
193
+ phase,
194
+ attempts,
195
+ reason: failure.reason,
196
+ host_error: failure.hostError
197
+ }
198
+ });
199
+ }
69
200
  function useParentBridge({
70
- trustedOrigins = EMPTY_TRUSTED_ORIGINS
201
+ trustedOrigins = EMPTY_TRUSTED_ORIGINS,
202
+ mintRetryPolicy = DEFAULT_MINT_RETRY_POLICY
71
203
  } = {}) {
72
204
  const isInIframe = typeof window !== "undefined" && window.parent !== window;
73
205
  const runtimeTrustedOrigins = useMemo(() => {
@@ -87,10 +219,14 @@ function useParentBridge({
87
219
  token: null,
88
220
  ...EMPTY_BRIDGE_URLS,
89
221
  hostAnswered: false,
90
- ready: false
222
+ ready: false,
223
+ error: null
91
224
  });
225
+ const mintRetryPolicyRef = useRef(mintRetryPolicy);
226
+ mintRetryPolicyRef.current = mintRetryPolicy;
92
227
  const readySignalSent = useRef(false);
93
228
  const configReceived = useRef(false);
229
+ const parentUrlsRef = useRef(EMPTY_BRIDGE_URLS);
94
230
  const tokenRef = useRef(state.token);
95
231
  tokenRef.current = state.token;
96
232
  const renewerRef = useRef(null);
@@ -107,15 +243,29 @@ function useParentBridge({
107
243
  };
108
244
  const tokenWaiters = /* @__PURE__ */ new Set();
109
245
  const handler = (event) => {
110
- if (!isTrustedOrigin({ origin: event.origin, trustedOrigins: runtimeTrustedOrigins })) {
246
+ if (event.source !== window.parent) return;
247
+ if (!isTrustedParentOrigin({ origin: event.origin, trustedOrigins: runtimeTrustedOrigins })) {
111
248
  return;
112
249
  }
113
250
  if (!event.data || typeof event.data !== "object") return;
114
251
  if (event.data.type === "athena-config") {
252
+ const { urls, rejected } = acceptBridgeConfigMessage({
253
+ previous: parentUrlsRef.current,
254
+ payload: event.data,
255
+ parentOrigin: event.origin,
256
+ locked: configReceived.current
257
+ });
115
258
  configReceived.current = true;
259
+ parentUrlsRef.current = urls;
260
+ for (const { field, reason } of rejected) {
261
+ athenaDiagnostics.emit("auth.bridge.config_rejected", {
262
+ level: "warn",
263
+ data: { mode: "iframe", field, reason }
264
+ });
265
+ }
116
266
  setState((prev) => ({
117
267
  ...prev,
118
- ...mergeBridgeConfigMessage(prev, event.data),
268
+ ...urls,
119
269
  hostAnswered: true
120
270
  // Don't set ready here — wait for athena-auth (token) or the timeout.
121
271
  // Setting ready on config alone causes a race: the thread list fires
@@ -179,147 +329,189 @@ function useParentBridge({
179
329
  let refreshTimer = null;
180
330
  let controller = null;
181
331
  let inFlight = null;
182
- let retryAttempt = 0;
332
+ let retryWait = null;
183
333
  const clearRefreshTimer = () => {
184
334
  if (refreshTimer) {
185
335
  clearTimeout(refreshTimer);
186
336
  refreshTimer = null;
187
337
  }
188
338
  };
189
- const markReady = () => {
190
- if (cancelled) return;
191
- setState((prev) => prev.ready ? prev : { ...prev, ready: true });
339
+ const endRetryWait = () => {
340
+ if (!retryWait) return;
341
+ clearTimeout(retryWait.timer);
342
+ const { resolve } = retryWait;
343
+ retryWait = null;
344
+ resolve();
192
345
  };
346
+ const waitBeforeRetry = (delayMs) => new Promise((resolve) => {
347
+ retryWait = { timer: setTimeout(endRetryWait, delayMs), resolve };
348
+ });
193
349
  const scheduleRefresh = (expiresAt) => {
194
350
  clearRefreshTimer();
195
- retryAttempt = 0;
196
351
  const delay = getTokenRefreshDelay(expiresAt, Date.now());
197
352
  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 });
353
+ refreshTimer = null;
354
+ void fetchAuth({ initial: false });
207
355
  }, delay);
208
356
  };
209
- const mintAuth = async ({
210
- markReadyOnFailure
211
- }) => {
357
+ const mintOnce = async (timeoutMs) => {
212
358
  const requestController = new AbortController();
213
359
  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;
360
+ let timedOut = false;
222
361
  const requestTimer = setTimeout(() => {
362
+ timedOut = true;
223
363
  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);
364
+ }, timeoutMs);
231
365
  try {
232
366
  const resp = await fetch("/_athena/auth", {
233
367
  credentials: "include",
234
368
  headers: { Accept: "application/json" },
235
369
  signal: requestController.signal
236
370
  });
237
- if (cancelled) return null;
371
+ if (cancelled) return { kind: "cancelled" };
372
+ if (resp.status === 404) return { kind: "absent", status: resp.status };
238
373
  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();
374
+ const hostError = await readHostError(resp);
375
+ if (cancelled) return { kind: "cancelled" };
376
+ if (timedOut && isRetryableMintStatus(resp.status)) {
377
+ return { kind: "transient", status: null, reason: "timeout", hostError: null };
258
378
  }
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
- });
379
+ return isRetryableMintStatus(resp.status) ? { kind: "transient", status: resp.status, reason: "status", hostError } : { kind: "refused", status: resp.status, reason: "status", hostError };
276
380
  }
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);
381
+ let data;
382
+ try {
383
+ data = await resp.json();
384
+ } catch (error) {
385
+ if (cancelled) return { kind: "cancelled" };
386
+ if (timedOut) {
387
+ return { kind: "transient", status: null, reason: "timeout", hostError: null, cause: error };
294
388
  }
389
+ return { kind: "refused", status: resp.status, reason: "invalid_body", hostError: null };
295
390
  }
296
- if (markReadyOnFailure) {
297
- markReady();
391
+ if (cancelled) return { kind: "cancelled" };
392
+ const token = data && typeof data === "object" ? data.token : null;
393
+ if (typeof token !== "string" || token === "") {
394
+ return { kind: "refused", status: resp.status, reason: "invalid_body", hostError: null };
298
395
  }
299
- if (!cancelled) {
300
- scheduleRetry();
396
+ return { kind: "minted", data };
397
+ } catch (error) {
398
+ if (cancelled) return { kind: "cancelled" };
399
+ if (timedOut) {
400
+ return { kind: "transient", status: null, reason: "timeout", hostError: null, cause: error };
301
401
  }
302
- return null;
402
+ return { kind: "transient", status: null, reason: "network", hostError: null, cause: error };
303
403
  } finally {
304
404
  clearTimeout(requestTimer);
305
405
  if (controller === requestController) controller = null;
306
406
  }
307
407
  };
408
+ const runMintCycle = async ({ initial }) => {
409
+ const policy = mintRetryPolicyRef.current;
410
+ const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
411
+ const phase = initial ? "initial" : "renewal";
412
+ const endBridgeSpan = initial ? athenaDiagnostics.span("auth.bridge", "auth.bridge.start", { mode: "marathon" }) : null;
413
+ let attempts = 0;
414
+ let lastFailure = null;
415
+ while (attempts < maxAttempts) {
416
+ attempts += 1;
417
+ const outcome = await mintOnce(policy.timeoutMs);
418
+ if (cancelled || outcome.kind === "cancelled") return null;
419
+ if (outcome.kind === "absent") {
420
+ endBridgeSpan == null ? void 0 : endBridgeSpan("auth.bridge.absent", {
421
+ data: { mode: "marathon", status: outcome.status }
422
+ });
423
+ setState(
424
+ (prev) => prev.ready && prev.error == null ? prev : { ...prev, ready: true, error: null }
425
+ );
426
+ return null;
427
+ }
428
+ if (outcome.kind === "minted") {
429
+ const { data } = outcome;
430
+ if (endBridgeSpan) {
431
+ endBridgeSpan("auth.bridge.ready", {
432
+ data: { mode: "marathon", has_token: typeof data.token === "string", attempts }
433
+ });
434
+ athenaDiagnostics.emit("auth.token.acquired", {
435
+ level: "debug",
436
+ data: { source: "marathon", expires_at: data.expires_at }
437
+ });
438
+ } else {
439
+ athenaDiagnostics.emit("auth.token.refreshed", {
440
+ level: "debug",
441
+ data: { source: "marathon", expires_at: data.expires_at, attempts }
442
+ });
443
+ }
444
+ const token = typeof data.token === "string" ? data.token : null;
445
+ setState({
446
+ token,
447
+ ...readAuthMintUrls(data),
448
+ hostAnswered: true,
449
+ ready: true,
450
+ error: null
451
+ });
452
+ tokenRef.current = token;
453
+ scheduleRefresh(data.expires_at);
454
+ return token;
455
+ }
456
+ lastFailure = outcome;
457
+ if (outcome.kind === "refused" || attempts >= maxAttempts) break;
458
+ const delayMs = getMintRetryDelay({ retry: attempts, policy });
459
+ athenaDiagnostics.emit("auth.bridge.retry", {
460
+ level: "warn",
461
+ data: {
462
+ mode: "marathon",
463
+ phase,
464
+ status: outcome.status,
465
+ reason: outcome.reason,
466
+ host_error: outcome.hostError,
467
+ attempt: attempts,
468
+ max_attempts: maxAttempts,
469
+ delay_ms: delayMs
470
+ }
471
+ });
472
+ await waitBeforeRetry(delayMs);
473
+ if (cancelled) return null;
474
+ }
475
+ if (!lastFailure) return null;
476
+ const error = mintFailureError({ failure: lastFailure, attempts, phase });
477
+ const failureData = {
478
+ mode: "marathon",
479
+ phase,
480
+ status: lastFailure.status,
481
+ reason: lastFailure.reason,
482
+ attempts
483
+ };
484
+ if (endBridgeSpan) {
485
+ endBridgeSpan(
486
+ error.code === "auth_bridge_timeout" ? "auth.bridge.timeout" : "auth.bridge.error",
487
+ { data: failureData, error }
488
+ );
489
+ } else {
490
+ athenaDiagnostics.emit("auth.token.refresh_failed", {
491
+ level: "error",
492
+ data: failureData,
493
+ error
494
+ });
495
+ }
496
+ setState((prev) => ({ ...prev, error, ready: true }));
497
+ return null;
498
+ };
308
499
  const fetchAuth = (options) => {
309
- inFlight ?? (inFlight = mintAuth(options).finally(() => {
500
+ inFlight ?? (inFlight = runMintCycle(options).finally(() => {
310
501
  inFlight = null;
311
502
  }));
312
503
  return inFlight;
313
504
  };
314
505
  renewerRef.current = ({ force }) => {
315
506
  if (!force) return Promise.resolve(tokenRef.current);
316
- return fetchAuth({ markReadyOnFailure: false }).then((token) => token ?? tokenRef.current);
507
+ return fetchAuth({ initial: false }).then((token) => token ?? tokenRef.current);
317
508
  };
318
- void fetchAuth({ markReadyOnFailure: true });
509
+ void fetchAuth({ initial: true });
319
510
  return () => {
320
511
  cancelled = true;
321
512
  controller == null ? void 0 : controller.abort();
322
513
  clearRefreshTimer();
514
+ endRetryWait();
323
515
  renewerRef.current = null;
324
516
  };
325
517
  }, [isInIframe]);
@@ -336,10 +528,17 @@ function useParentAuth() {
336
528
  return useParentBridge().token;
337
529
  }
338
530
  export {
339
- getAuthRetryDelay,
531
+ DEFAULT_MINT_RETRY_POLICY,
532
+ acceptBridgeConfigMessage,
533
+ getMintRetryDelay,
340
534
  getTokenRefreshDelay,
535
+ isCustomerAppHost,
536
+ isRetryableMintStatus,
537
+ isTrustedParentOrigin,
341
538
  mergeBridgeConfigMessage,
539
+ mintFailureError,
342
540
  readAuthMintUrls,
541
+ refuseBridgeUrl,
343
542
  useParentAuth,
344
543
  useParentBridge
345
544
  };