@athenaintel/react 0.26.5 → 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.
- package/README.md +19 -0
- package/dist/chat/AthenaChat.cjs +1 -1
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +1 -1
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireQueuedMessages.cjs +44 -1
- package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
- package/dist/chat/StatewireQueuedMessages.js +45 -2
- package/dist/chat/StatewireQueuedMessages.js.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.cjs +6 -0
- package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.d.ts +2 -0
- package/dist/chat-ui/mentions/mention-chip.js +6 -0
- package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
- package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
- package/dist/diagnostics/athena-diagnostics.d.ts +1 -1
- package/dist/diagnostics/athena-diagnostics.js.map +1 -1
- package/dist/diagnostics/errors.cjs +11 -2
- package/dist/diagnostics/errors.cjs.map +1 -1
- package/dist/diagnostics/errors.d.ts +11 -2
- package/dist/diagnostics/errors.js +11 -2
- package/dist/diagnostics/errors.js.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.cjs +15 -0
- package/dist/packages/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.js +15 -0
- package/dist/packages/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/provider/AthenaProvider.cjs +3 -0
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +3 -0
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/runtime/auth.cjs +219 -104
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +79 -5
- package/dist/runtime/auth.js +219 -104
- package/dist/runtime/auth.js.map +1 -1
- package/package.json +1 -1
package/dist/runtime/auth.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
81
|
-
*
|
|
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.
|
package/dist/runtime/auth.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
190
|
-
if (
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
|
210
|
-
markReadyOnFailure
|
|
211
|
-
}) => {
|
|
277
|
+
const mintOnce = async (timeoutMs) => {
|
|
212
278
|
const requestController = new AbortController();
|
|
213
279
|
controller = requestController;
|
|
214
|
-
|
|
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
|
-
|
|
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
|
|
291
|
+
if (cancelled) return { kind: "cancelled" };
|
|
292
|
+
if (resp.status === 404) return { kind: "absent", status: resp.status };
|
|
238
293
|
if (!resp.ok) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 (
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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 =
|
|
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({
|
|
427
|
+
return fetchAuth({ initial: false }).then((token) => token ?? tokenRef.current);
|
|
317
428
|
};
|
|
318
|
-
void fetchAuth({
|
|
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
|
-
|
|
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
|