@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.
- package/README.md +32 -1
- package/dist/chat/StatewireApprovalCard.cjs +19 -0
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.js +19 -0
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/message-time.cjs.map +1 -1
- package/dist/chat/message-time.d.ts +4 -3
- package/dist/chat/message-time.js.map +1 -1
- package/dist/composer/QuoteContext.cjs +13 -1
- package/dist/composer/QuoteContext.cjs.map +1 -1
- package/dist/composer/QuoteContext.d.ts +11 -1
- package/dist/composer/QuoteContext.js +13 -1
- package/dist/composer/QuoteContext.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/lib/athena-urls.cjs +1 -1
- package/dist/lib/athena-urls.cjs.map +1 -1
- package/dist/lib/athena-urls.js +1 -1
- package/dist/lib/athena-urls.js.map +1 -1
- package/dist/lib/posthog/mask-sensitive-input.cjs +34 -0
- package/dist/lib/posthog/mask-sensitive-input.cjs.map +1 -0
- package/dist/lib/posthog/mask-sensitive-input.d.ts +9 -0
- package/dist/lib/posthog/mask-sensitive-input.js +34 -0
- package/dist/lib/posthog/mask-sensitive-input.js.map +1 -0
- package/dist/packages/agent-runtime/src/conversation-index.cjs +2 -1
- package/dist/packages/agent-runtime/src/conversation-index.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/conversation-index.js +2 -1
- package/dist/packages/agent-runtime/src/conversation-index.js.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +2 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +2 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs +26 -8
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +26 -8
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/agent-runtime/src/messages.cjs +70 -14
- package/dist/packages/agent-runtime/src/messages.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/messages.js +71 -15
- package/dist/packages/agent-runtime/src/messages.js.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.cjs +75 -0
- package/dist/packages/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.js +75 -0
- package/dist/packages/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/packages/agent-runtime/src/tool-result.cjs +19 -4
- package/dist/packages/agent-runtime/src/tool-result.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/tool-result.js +19 -4
- package/dist/packages/agent-runtime/src/tool-result.js.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs +23 -4
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.js +24 -5
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.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/provider/PostHogProvider.cjs +3 -1
- package/dist/provider/PostHogProvider.cjs.map +1 -1
- package/dist/provider/PostHogProvider.js +3 -1
- package/dist/provider/PostHogProvider.js.map +1 -1
- package/dist/runtime/auth.cjs +307 -108
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +151 -10
- package/dist/runtime/auth.js +307 -108
- package/dist/runtime/auth.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/dist/runtime/auth.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { useMemo, useState, useRef, useEffect, useCallback } from "react";
|
|
2
2
|
import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
-
...
|
|
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
|
|
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
|
|
190
|
-
if (
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
|
210
|
-
markReadyOnFailure
|
|
211
|
-
}) => {
|
|
357
|
+
const mintOnce = async (timeoutMs) => {
|
|
212
358
|
const requestController = new AbortController();
|
|
213
359
|
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;
|
|
360
|
+
let timedOut = false;
|
|
222
361
|
const requestTimer = setTimeout(() => {
|
|
362
|
+
timedOut = true;
|
|
223
363
|
requestController.abort();
|
|
224
|
-
|
|
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
|
|
371
|
+
if (cancelled) return { kind: "cancelled" };
|
|
372
|
+
if (resp.status === 404) return { kind: "absent", status: resp.status };
|
|
238
373
|
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();
|
|
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
|
|
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
|
-
|
|
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);
|
|
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 (
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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 =
|
|
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({
|
|
507
|
+
return fetchAuth({ initial: false }).then((token) => token ?? tokenRef.current);
|
|
317
508
|
};
|
|
318
|
-
void fetchAuth({
|
|
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
|
-
|
|
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
|
};
|