@athenaintel/react 0.56.0 → 0.57.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 +71 -1
- package/dist/agent-runtime/src/inbound-channel.cjs +96 -0
- package/dist/agent-runtime/src/inbound-channel.cjs.map +1 -0
- package/dist/agent-runtime/src/inbound-channel.d.ts +75 -0
- package/dist/agent-runtime/src/inbound-channel.js +96 -0
- package/dist/agent-runtime/src/inbound-channel.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +2 -1
- package/dist/agent-runtime/src/interrupts.cjs +45 -3
- package/dist/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/agent-runtime/src/interrupts.d.ts +51 -4
- package/dist/agent-runtime/src/interrupts.js +46 -4
- package/dist/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
- package/dist/agent-runtime/src/offline-status.d.ts +25 -0
- package/dist/agent-runtime/src/offline-status.js.map +1 -1
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +8 -8
- package/dist/agent-runtime/src/tool-metadata.cjs +8 -0
- package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.js +8 -0
- package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/auth/AthenaSSOProvider.cjs +54 -12
- package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
- package/dist/auth/AthenaSSOProvider.d.ts +23 -2
- package/dist/auth/AthenaSSOProvider.js +54 -12
- package/dist/auth/AthenaSSOProvider.js.map +1 -1
- package/dist/auth/index.cjs +2 -0
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/sign-back-in.cjs +141 -0
- package/dist/auth/sign-back-in.cjs.map +1 -0
- package/dist/auth/sign-back-in.d.ts +117 -0
- package/dist/auth/sign-back-in.js +141 -0
- package/dist/auth/sign-back-in.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +7 -2
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +7 -2
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireApprovalCard.cjs +6 -2
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +8 -1
- package/dist/chat/StatewireApprovalCard.js +7 -3
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat-ui/index.cjs +4 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +1 -0
- package/dist/chat-ui/index.js +5 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/messages/inbound-channel-message.cjs +219 -0
- package/dist/chat-ui/messages/inbound-channel-message.cjs.map +1 -0
- package/dist/chat-ui/messages/inbound-channel-message.d.ts +60 -0
- package/dist/chat-ui/messages/inbound-channel-message.js +219 -0
- package/dist/chat-ui/messages/inbound-channel-message.js.map +1 -0
- package/dist/chat-ui/messages/inbound-channel-reader.cjs +184 -0
- package/dist/chat-ui/messages/inbound-channel-reader.cjs.map +1 -0
- package/dist/chat-ui/messages/inbound-channel-reader.d.ts +65 -0
- package/dist/chat-ui/messages/inbound-channel-reader.js +184 -0
- package/dist/chat-ui/messages/inbound-channel-reader.js.map +1 -0
- package/dist/chat-ui/messages/message-components.cjs +18 -14
- package/dist/chat-ui/messages/message-components.cjs.map +1 -1
- package/dist/chat-ui/messages/message-components.d.ts +7 -0
- package/dist/chat-ui/messages/message-components.js +19 -15
- package/dist/chat-ui/messages/message-components.js.map +1 -1
- package/dist/composer/sign-back-in-draft.cjs +62 -0
- package/dist/composer/sign-back-in-draft.cjs.map +1 -0
- package/dist/composer/sign-back-in-draft.d.ts +34 -0
- package/dist/composer/sign-back-in-draft.js +62 -0
- package/dist/composer/sign-back-in-draft.js.map +1 -0
- package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
- package/dist/diagnostics/athena-diagnostics.d.ts +6 -1
- package/dist/diagnostics/athena-diagnostics.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +48 -3
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +49 -4
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/auth.cjs +11 -0
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +18 -1
- package/dist/runtime/auth.js +11 -0
- package/dist/runtime/auth.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
|
|
|
7
7
|
const errors = require("../diagnostics/errors.cjs");
|
|
8
8
|
const auth = require("../runtime/auth.cjs");
|
|
9
9
|
const AthenaAuthContext = require("./AthenaAuthContext.cjs");
|
|
10
|
+
const signBackIn = require("./sign-back-in.cjs");
|
|
10
11
|
const SSO_USERINFO_RETRY_POLICY = {
|
|
11
12
|
...auth.DEFAULT_MINT_RETRY_POLICY,
|
|
12
13
|
timeoutMs: 1e4
|
|
@@ -28,7 +29,9 @@ async function readUserInfoOnce({
|
|
|
28
29
|
}, timeoutMs);
|
|
29
30
|
try {
|
|
30
31
|
const response = await fetch(url, { credentials: "include", signal: request.signal });
|
|
31
|
-
if (response.status === 401 || response.status === 403)
|
|
32
|
+
if (response.status === 401 || response.status === 403) {
|
|
33
|
+
return { kind: "unauthenticated", status: response.status === 401 ? 401 : 403 };
|
|
34
|
+
}
|
|
32
35
|
if (!response.ok) {
|
|
33
36
|
return auth.isRetryableMintStatus(response.status) ? { kind: "transient", status: response.status, reason: "status" } : { kind: "failed", status: response.status };
|
|
34
37
|
}
|
|
@@ -57,7 +60,9 @@ async function readSession({
|
|
|
57
60
|
for (let attempts = 1; ; attempts += 1) {
|
|
58
61
|
const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });
|
|
59
62
|
if (attempt.kind === "session") return { kind: "session", data: attempt.data };
|
|
60
|
-
if (attempt.kind === "unauthenticated")
|
|
63
|
+
if (attempt.kind === "unauthenticated") {
|
|
64
|
+
return { kind: "no_session", reason: "unauthenticated", status: attempt.status };
|
|
65
|
+
}
|
|
61
66
|
if (attempt.kind === "invalid_response") {
|
|
62
67
|
console.error(
|
|
63
68
|
'[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
|
|
@@ -177,8 +182,10 @@ function AthenaSSOProvider({
|
|
|
177
182
|
displayOnError,
|
|
178
183
|
onError,
|
|
179
184
|
revalidateOnWindowFocus = false,
|
|
185
|
+
signBackInOnSessionEnd = true,
|
|
180
186
|
retryPolicy = SSO_USERINFO_RETRY_POLICY
|
|
181
187
|
}) {
|
|
188
|
+
var _a, _b;
|
|
182
189
|
const [queryClient] = require$$0.useState(() => new reactQuery.QueryClient());
|
|
183
190
|
const retryPolicyRef = require$$0.useRef(retryPolicy);
|
|
184
191
|
retryPolicyRef.current = retryPolicy;
|
|
@@ -207,14 +214,14 @@ function AthenaSSOProvider({
|
|
|
207
214
|
const queryKey = require$$0.useMemo(() => ["athena-sdk", "sso-session", ssoUrls.userInfo], [ssoUrls.userInfo]);
|
|
208
215
|
const queryFn = require$$0.useCallback(
|
|
209
216
|
async ({ signal }) => {
|
|
210
|
-
var
|
|
217
|
+
var _a2;
|
|
211
218
|
try {
|
|
212
219
|
return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });
|
|
213
220
|
} catch (readError) {
|
|
214
221
|
if (readError instanceof errors.AthenaSdkError) {
|
|
215
222
|
athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.error", { error: readError });
|
|
216
223
|
try {
|
|
217
|
-
(
|
|
224
|
+
(_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, readError);
|
|
218
225
|
} catch (callbackError) {
|
|
219
226
|
console.error("[AthenaSDK] AthenaSSOProvider onError threw", callbackError);
|
|
220
227
|
}
|
|
@@ -244,6 +251,35 @@ function AthenaSSOProvider({
|
|
|
244
251
|
const loading = query.isPending;
|
|
245
252
|
const reason = session ? null : (answer == null ? void 0 : answer.kind) === "no_session" ? answer.reason : query.isError ? "request_failed" : null;
|
|
246
253
|
const signedOut = (answer == null ? void 0 : answer.kind) === "no_session" && answer.signedOut === true;
|
|
254
|
+
const answerStatus = (answer == null ? void 0 : answer.kind) === "no_session" ? answer.status : void 0;
|
|
255
|
+
const servedSessionRef = require$$0.useRef(null);
|
|
256
|
+
if (session !== null) servedSessionRef.current = session;
|
|
257
|
+
const servedSession = servedSessionRef.current;
|
|
258
|
+
const endedAt = !loading && servedSession !== null && reason === "unauthenticated" && !signedOut ? query.dataUpdatedAt : null;
|
|
259
|
+
const endingRef = require$$0.useRef(null);
|
|
260
|
+
if (endedAt !== null && ((_a = endingRef.current) == null ? void 0 : _a.at) !== endedAt) {
|
|
261
|
+
endingRef.current = {
|
|
262
|
+
at: endedAt,
|
|
263
|
+
signBackIn: signBackInOnSessionEnd && answerStatus === 401 && signBackIn.canSignBackIn()
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const [unsentEnding, setUnsentEnding] = require$$0.useState(null);
|
|
267
|
+
const signingBackIn = endedAt !== null && ((_b = endingRef.current) == null ? void 0 : _b.at) === endedAt && endingRef.current.signBackIn && unsentEnding !== endedAt;
|
|
268
|
+
require$$0.useEffect(() => {
|
|
269
|
+
if (endedAt === null) return;
|
|
270
|
+
if (!signingBackIn) {
|
|
271
|
+
if (unsentEnding !== endedAt) {
|
|
272
|
+
signBackIn.reportSessionEnded({ source: "sso", status: answerStatus, signBackIn: false });
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!signBackIn.signBackIn({ loginUrl: ssoUrls.login, source: "sso", status: answerStatus })) {
|
|
277
|
+
setUnsentEnding(endedAt);
|
|
278
|
+
}
|
|
279
|
+
}, [endedAt, signingBackIn, unsentEnding, ssoUrls.login, answerStatus]);
|
|
280
|
+
require$$0.useEffect(() => {
|
|
281
|
+
signBackIn.restoreSignBackInHash();
|
|
282
|
+
}, []);
|
|
247
283
|
require$$0.useEffect(() => {
|
|
248
284
|
if (!revalidateOnWindowFocus || typeof window === "undefined") {
|
|
249
285
|
return;
|
|
@@ -264,10 +300,15 @@ function AthenaSSOProvider({
|
|
|
264
300
|
};
|
|
265
301
|
}, [revalidateOnWindowFocus, queryClient, queryKey]);
|
|
266
302
|
require$$0.useEffect(() => {
|
|
267
|
-
if (
|
|
268
|
-
|
|
303
|
+
if (loading || reason !== "unauthenticated" || signedOut || signingBackIn || displayOnError || typeof window === "undefined") {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!signBackIn.redirectToSignIn({ loginUrl: ssoUrls.login })) {
|
|
307
|
+
console.warn(
|
|
308
|
+
"[AthenaSDK] Not sending the page to sign-in again: this tab went through sign-in less than two minutes ago and still has no session. Pass `displayOnError` to show a sign-in screen instead."
|
|
309
|
+
);
|
|
269
310
|
}
|
|
270
|
-
}, [loading, reason, signedOut, displayOnError, ssoUrls.login]);
|
|
311
|
+
}, [loading, reason, signedOut, signingBackIn, displayOnError, ssoUrls.login]);
|
|
271
312
|
const retry = require$$0.useCallback(() => {
|
|
272
313
|
void queryClient.resetQueries({ queryKey });
|
|
273
314
|
}, [queryClient, queryKey]);
|
|
@@ -294,7 +335,7 @@ function AthenaSSOProvider({
|
|
|
294
335
|
sessionRef.current = session;
|
|
295
336
|
const getToken = require$$0.useCallback(
|
|
296
337
|
async ({ force = false } = {}) => {
|
|
297
|
-
var
|
|
338
|
+
var _a2;
|
|
298
339
|
if (force) {
|
|
299
340
|
try {
|
|
300
341
|
const renewed = await queryClient.fetchQuery({
|
|
@@ -308,10 +349,11 @@ function AthenaSSOProvider({
|
|
|
308
349
|
} catch {
|
|
309
350
|
}
|
|
310
351
|
}
|
|
311
|
-
return ((
|
|
352
|
+
return ((_a2 = sessionRef.current) == null ? void 0 : _a2.accessToken) ?? null;
|
|
312
353
|
},
|
|
313
354
|
[queryClient, queryKey, queryFn]
|
|
314
355
|
);
|
|
356
|
+
const shownSession = session ?? (signingBackIn ? servedSession : null);
|
|
315
357
|
const authState = require$$0.useMemo(() => {
|
|
316
358
|
if (loading) {
|
|
317
359
|
return {
|
|
@@ -325,7 +367,7 @@ function AthenaSSOProvider({
|
|
|
325
367
|
getToken
|
|
326
368
|
};
|
|
327
369
|
}
|
|
328
|
-
if (!
|
|
370
|
+
if (!shownSession) {
|
|
329
371
|
return {
|
|
330
372
|
isLoggedIn: false,
|
|
331
373
|
isLoading: false,
|
|
@@ -336,7 +378,7 @@ function AthenaSSOProvider({
|
|
|
336
378
|
getToken
|
|
337
379
|
};
|
|
338
380
|
}
|
|
339
|
-
const { user, orgs, accessToken } = mapSSOResponse(
|
|
381
|
+
const { user, orgs, accessToken } = mapSSOResponse(shownSession);
|
|
340
382
|
return {
|
|
341
383
|
isLoggedIn: true,
|
|
342
384
|
isLoading: false,
|
|
@@ -346,7 +388,7 @@ function AthenaSSOProvider({
|
|
|
346
388
|
logout,
|
|
347
389
|
getToken
|
|
348
390
|
};
|
|
349
|
-
}, [loading,
|
|
391
|
+
}, [loading, shownSession, logout, getToken]);
|
|
350
392
|
if (authState.isLoading) {
|
|
351
393
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayWhileLoading ?? null });
|
|
352
394
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AthenaSSOProvider.cjs","sources":["../../src/auth/AthenaSSOProvider.tsx"],"sourcesContent":["import { QueryClient, useQuery } from '@tanstack/react-query';\nimport type { ReactNode } from 'react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { AthenaSdkError } from '../diagnostics/errors';\nimport {\n DEFAULT_MINT_RETRY_POLICY,\n getMintRetryDelay,\n isRetryableMintStatus,\n type MintRetryPolicy,\n} from '../runtime/auth';\nimport type { AthenaStatewireTokenGetter } from '../runtime/statewire-auth-headers';\nimport { AthenaAuthContext } from './AthenaAuthContext';\nimport type {\n AthenaAuthState,\n AthenaOrg,\n AthenaSSOUserInfo,\n AthenaUser,\n} from './types';\n\n// ─── Props ────────────────────────────────────────────────────────────\n\nexport interface AthenaSSOProviderProps {\n children: ReactNode;\n\n /**\n * Base URL used to resolve the Athena SSO endpoints.\n *\n * When omitted, the provider defaults to same-origin relative paths:\n * `/api/sso/userinfo`, `/api/sso/initiate`, and `/api/sso/logout`.\n *\n * Use this when your frontend and backend live on different origins\n * (for example `https://app.example.com` + `https://api.example.com`).\n */\n ssoBaseUrl?: string;\n\n /**\n * Optional override for the SSO user-info endpoint that returns the\n * authenticated user's data and access token.\n *\n * The endpoint must return JSON matching `AthenaSSOUserInfo`:\n * ```json\n * {\n * \"user\": { \"user_id\": \"…\", \"email\": \"…\", … },\n * \"orgMemberInfos\": [{ \"orgId\": \"…\", \"orgName\": \"…\", \"urlSafeOrgName\": \"…\" }],\n * \"accessToken\": \"…\"\n * }\n * ```\n *\n * The request is sent with `credentials: 'include'` so that\n * session cookies are forwarded automatically.\n */\n ssoUserInfoUrl?: string;\n\n /**\n * Optional override for the URL to redirect the user to when no valid\n * SSO session exists.\n * This is typically the SSO initiation endpoint on your backend\n * (e.g. `https://api.yourdomain.com/api/sso/initiate`).\n */\n ssoLoginUrl?: string;\n\n /**\n * Optional override for the backend SSO logout endpoint.\n * Defaults to `/api/sso/logout` (or `${ssoBaseUrl}/api/sso/logout`).\n */\n ssoLogoutUrl?: string;\n\n /**\n * Custom element displayed while the SSO session is being verified.\n * Defaults to `null` (renders nothing).\n */\n displayWhileLoading?: ReactNode;\n\n /**\n * Shown when there is no session to sign the app in with: an element, or a\n * function that receives why (`reason`) and a `retry`, so the app can offer\n * sign-in only to a person who is signed out and a retry when the endpoint\n * could not answer (see `AthenaSSOFailure`).\n * If not provided, the user is automatically redirected to `ssoLoginUrl`\n * when the endpoint answers that there is no session.\n */\n displayOnError?: ReactNode | ((failure: AthenaSSOFailure) => ReactNode);\n\n /**\n * Called with the typed error (`auth_sso_failed`) when a session read gives\n * up. A nested `AthenaProvider`'s `onError` is not mounted while the first\n * read runs, so observe sign-in failures here.\n */\n onError?: (error: AthenaSdkError) => void;\n\n /**\n * Re-validate the SSO session when the window regains focus.\n * Disabled by default. When enabled, token expiry and user switching\n * are picked up without requiring a full page reload.\n */\n revalidateOnWindowFocus?: boolean;\n\n /** @internal Overrides how a session read retries; used by tests. */\n retryPolicy?: MintRetryPolicy;\n}\n\n/**\n * Why the provider has no session.\n *\n * - `unauthenticated`: the endpoint answered 401 or 403, or the person signed\n * out. Offer sign-in.\n * - `request_failed`: the endpoint did not answer usably. A network error, a\n * timeout, or a 5xx / 408 / 429 on every attempt of one read, or another\n * status. The person may well be signed in. Offer `retry`.\n * - `invalid_response`: it answered without a usable session.\n */\nexport type AthenaSSOFailureReason = 'unauthenticated' | 'invalid_response' | 'request_failed';\n\n/** What `displayOnError` receives when it is a function. */\nexport interface AthenaSSOFailure {\n reason: AthenaSSOFailureReason;\n /** Read the session again. `displayWhileLoading` shows meanwhile. */\n retry: () => void;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────\n\n/**\n * How one session read retries: the parent bridge's mint policy (Marathon's\n * `/api/sso/userinfo` and `/_athena/auth` mint the same credential), with a\n * longer wait per request, since every request mints and a slow answer is\n * still an answer.\n */\nconst SSO_USERINFO_RETRY_POLICY: MintRetryPolicy = {\n ...DEFAULT_MINT_RETRY_POLICY,\n timeoutMs: 10_000,\n};\n\n/** One userinfo request, classified. */\ntype UserInfoAttempt =\n | { kind: 'session'; data: AthenaSSOUserInfo }\n | { kind: 'unauthenticated' }\n | { kind: 'invalid_response' }\n /** A status it does not retry, or a 2xx whose body is not JSON. */\n | { kind: 'failed'; status: number }\n | {\n kind: 'transient';\n status: number | null;\n reason: 'status' | 'network' | 'timeout';\n cause?: unknown;\n };\n\nconst abortError = () => new DOMException('The session read was cancelled.', 'AbortError');\n\n/** One userinfo request. Throws an `AbortError` when `signal` cancels the read. */\nasync function readUserInfoOnce({\n url,\n signal,\n timeoutMs,\n}: {\n url: string;\n signal: AbortSignal;\n timeoutMs: number;\n}): Promise<UserInfoAttempt> {\n if (signal.aborted) throw abortError();\n const request = new AbortController();\n const cancel = () => request.abort();\n signal.addEventListener('abort', cancel, { once: true });\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n request.abort();\n }, timeoutMs);\n\n try {\n const response = await fetch(url, { credentials: 'include', signal: request.signal });\n if (response.status === 401 || response.status === 403) return { kind: 'unauthenticated' };\n if (!response.ok) {\n return isRetryableMintStatus(response.status)\n ? { kind: 'transient', status: response.status, reason: 'status' }\n : { kind: 'failed', status: response.status };\n }\n\n let data: unknown;\n try {\n data = await response.json();\n } catch (error) {\n if (signal.aborted) throw abortError();\n return timedOut\n ? { kind: 'transient', status: null, reason: 'timeout', cause: error }\n : { kind: 'failed', status: response.status };\n }\n return isValidSSOUserInfo(data) ? { kind: 'session', data } : { kind: 'invalid_response' };\n } catch (error) {\n if (signal.aborted) throw abortError();\n return { kind: 'transient', status: null, reason: timedOut ? 'timeout' : 'network', cause: error };\n } finally {\n clearTimeout(timer);\n signal.removeEventListener('abort', cancel);\n }\n}\n\n/**\n * What a read concluded, as the query holds it. A read that could not answer\n * throws instead (`request_failed`), so an earlier answer stays.\n */\ntype SessionAnswer =\n | { kind: 'session'; data: AthenaSSOUserInfo }\n | {\n kind: 'no_session';\n reason: Exclude<AthenaSSOFailureReason, 'request_failed'>;\n /** The person signed out here (`logout`), rather than the endpoint answering no session. */\n signedOut?: true;\n };\n\n/**\n * One session read: one bounded cycle of userinfo requests. A transient\n * failure is retried with `policy`'s capped, jittered backoff; a read that\n * gives up throws the typed error it reports. Throws an `AbortError` when\n * `signal` cancels the read.\n */\nasync function readSession({\n url,\n signal,\n policy,\n}: {\n url: string;\n signal: AbortSignal;\n policy: MintRetryPolicy;\n}): Promise<SessionAnswer> {\n const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));\n for (let attempts = 1; ; attempts += 1) {\n const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });\n\n if (attempt.kind === 'session') return { kind: 'session', data: attempt.data };\n if (attempt.kind === 'unauthenticated') return { kind: 'no_session', reason: 'unauthenticated' };\n if (attempt.kind === 'invalid_response') {\n console.error(\n '[AthenaSDK] Invalid SSO userinfo response: expected \"user.user_id\", \"user.email\", \"orgMemberInfos\", and \"accessToken\"',\n );\n return { kind: 'no_session', reason: 'invalid_response' };\n }\n\n if (attempt.kind === 'transient' && attempts < maxAttempts) {\n const delayMs = getMintRetryDelay({ retry: attempts, policy });\n athenaDiagnostics.emit('auth.sso.userinfo.retry', {\n level: 'warn',\n data: {\n status: attempt.status,\n reason: attempt.reason,\n attempt: attempts,\n max_attempts: maxAttempts,\n delay_ms: delayMs,\n },\n });\n await waitBeforeRetry({ delayMs, signal });\n continue;\n }\n\n throw sessionReadError({ url, attempt, attempts });\n }\n}\n\n/** The backoff between two requests of one read; cancelling the read ends it at once. */\nfunction waitBeforeRetry({ delayMs, signal }: { delayMs: number; signal: AbortSignal }): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal.aborted) {\n reject(abortError());\n return;\n }\n const onAbort = () => {\n clearTimeout(timer);\n reject(abortError());\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, delayMs);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** The typed error a read reports when it gives up without an answer about the session. */\nfunction sessionReadError({\n url,\n attempt,\n attempts,\n}: {\n url: string;\n attempt: Extract<UserInfoAttempt, { kind: 'failed' | 'transient' }>;\n attempts: number;\n}): AthenaSdkError {\n if (attempt.kind === 'failed') {\n const unreadable = attempt.status >= 200 && attempt.status < 300;\n return new AthenaSdkError({\n code: 'auth_sso_failed',\n message: unreadable\n ? `The SSO session read (${url}) answered HTTP ${attempt.status} without a JSON body.`\n : `The SSO session read (${url}) answered HTTP ${attempt.status}.`,\n hint: 'Check that ssoBaseUrl / ssoUserInfoUrl point at the SSO userinfo endpoint.',\n status: attempt.status,\n context: { attempts, reason: unreadable ? 'invalid_body' : 'status' },\n });\n }\n const answer =\n attempt.status !== null\n ? `HTTP ${attempt.status}`\n : attempt.reason === 'timeout'\n ? 'a timeout'\n : 'a network error';\n return new AthenaSdkError({\n code: 'auth_sso_failed',\n message: `The SSO session read (${url}) failed ${attempts} time${attempts === 1 ? '' : 's'} (last: ${answer}).`,\n hint: 'The sign-in service did not answer; the person may still be signed in. Offer a retry (displayOnError receives retry).',\n ...(attempt.status !== null && { status: attempt.status }),\n ...(attempt.cause !== undefined && { cause: attempt.cause }),\n context: { attempts, reason: attempt.reason },\n });\n}\n\nconst DEFAULT_SSO_PATHS = {\n userInfo: '/api/sso/userinfo',\n login: '/api/sso/initiate',\n logout: '/api/sso/logout',\n} as const;\n\nconst trimTrailingSlash = (value: string): string => value.replace(/\\/+$/, '');\n\nfunction resolveSSOUrl({\n baseUrl,\n overrideUrl,\n defaultPath,\n}: {\n baseUrl?: string;\n overrideUrl?: string;\n defaultPath: string;\n}): string {\n if (overrideUrl) {\n return overrideUrl;\n }\n\n if (!baseUrl) {\n return defaultPath;\n }\n\n return `${trimTrailingSlash(baseUrl)}${defaultPath}`;\n}\n\nfunction isValidSSOUserInfo(data: unknown): data is AthenaSSOUserInfo {\n if (!data || typeof data !== 'object') {\n return false;\n }\n\n const candidate = data as {\n accessToken?: unknown;\n orgMemberInfos?: unknown;\n user?: {\n user_id?: unknown;\n email?: unknown;\n };\n };\n\n return (\n typeof candidate.accessToken === 'string' &&\n Array.isArray(candidate.orgMemberInfos) &&\n !!candidate.user &&\n typeof candidate.user.user_id === 'string' &&\n typeof candidate.user.email === 'string'\n );\n}\n\n/** Map the backend SSO response to the SDK's user/org types. */\nfunction mapSSOResponse(data: AthenaSSOUserInfo): {\n user: AthenaUser;\n orgs: AthenaOrg[];\n accessToken: string;\n} {\n const user: AthenaUser = {\n userId: data.user.user_id,\n email: data.user.email,\n firstName: data.user.first_name || undefined,\n lastName: data.user.last_name || undefined,\n username: data.user.username || undefined,\n pictureUrl: data.user.picture_url || undefined,\n properties: data.user.properties,\n };\n\n const orgs: AthenaOrg[] = data.orgMemberInfos.map((org) => ({\n orgId: org.orgId,\n orgName: org.orgName,\n urlSafeOrgName: org.urlSafeOrgName,\n }));\n\n return { user, orgs, accessToken: data.accessToken };\n}\n\n// ─── Provider ─────────────────────────────────────────────────────────\n\n/**\n * Wraps your app with Athena authentication powered by an external SSO\n * identity provider (e.g. Microsoft Entra / Azure AD, Okta, etc.).\n *\n * The SSO login flow is handled entirely by your backend. This provider\n * verifies the session by fetching `ssoUserInfoUrl` and exposes the\n * same `AthenaAuthState` context that `AthenaAuthProvider` (PropelAuth)\n * provides, so downstream components like `<AthenaProvider>` and\n * `useAthenaAuth()` work identically.\n *\n * Place this **above** `<AthenaProvider>` in the component tree.\n *\n * @example\n * ```tsx\n * import { AthenaSSOProvider } from '@athenaintel/react/auth';\n * import { AthenaProvider, AthenaChat } from '@athenaintel/react';\n *\n * function App() {\n * return (\n * <AthenaSSOProvider\n * ssoUserInfoUrl=\"https://api.yourdomain.com/api/sso/userinfo\"\n * ssoLoginUrl=\"https://api.yourdomain.com/api/sso/initiate\"\n * >\n * <AthenaProvider>\n * <AthenaChat />\n * </AthenaProvider>\n * </AthenaSSOProvider>\n * );\n * }\n * ```\n */\nexport function AthenaSSOProvider({\n children,\n ssoBaseUrl,\n ssoUserInfoUrl,\n ssoLoginUrl,\n ssoLogoutUrl,\n displayWhileLoading,\n displayOnError,\n onError,\n revalidateOnWindowFocus = false,\n retryPolicy = SSO_USERINFO_RETRY_POLICY,\n}: AthenaSSOProviderProps) {\n // The provider's own client, never a host's: the session holds an access\n // token, and a host may persist its query cache.\n const [queryClient] = useState(() => new QueryClient());\n const retryPolicyRef = useRef(retryPolicy);\n retryPolicyRef.current = retryPolicy;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n\n const ssoUrls = useMemo(\n () => ({\n userInfo: resolveSSOUrl({\n baseUrl: ssoBaseUrl,\n overrideUrl: ssoUserInfoUrl,\n defaultPath: DEFAULT_SSO_PATHS.userInfo,\n }),\n login: resolveSSOUrl({\n baseUrl: ssoBaseUrl,\n overrideUrl: ssoLoginUrl,\n defaultPath: DEFAULT_SSO_PATHS.login,\n }),\n logout: resolveSSOUrl({\n baseUrl: ssoBaseUrl,\n overrideUrl: ssoLogoutUrl,\n defaultPath: DEFAULT_SSO_PATHS.logout,\n }),\n }),\n [ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl],\n );\n\n const queryKey = useMemo(() => ['athena-sdk', 'sso-session', ssoUrls.userInfo], [ssoUrls.userInfo]);\n\n // One read is one bounded cycle (`readSession`). A read that gives up\n // throws, so the query keeps the session an earlier read answered: a failed\n // background revalidation or renewal does not sign the person out.\n const queryFn = useCallback(\n async ({ signal }: { signal: AbortSignal }): Promise<SessionAnswer> => {\n try {\n return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });\n } catch (readError) {\n if (readError instanceof AthenaSdkError) {\n athenaDiagnostics.emit('auth.sso.userinfo.error', { error: readError });\n try {\n onErrorRef.current?.(readError);\n } catch (callbackError) {\n console.error('[AthenaSDK] AthenaSSOProvider onError threw', callbackError);\n }\n }\n throw readError;\n }\n },\n [ssoUrls.userInfo],\n );\n\n const query = useQuery(\n {\n queryKey,\n queryFn,\n // `readSession` retries a transient failure itself, with the bridge's\n // policy; the query never retries on top of it, and reads while the\n // browser reports itself offline too, so that policy decides.\n retry: false,\n networkMode: 'always',\n staleTime: Number.POSITIVE_INFINITY,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n },\n queryClient,\n );\n\n const answer = query.data;\n const session = answer?.kind === 'session' ? answer.data : null;\n const loading = query.isPending;\n const reason: AthenaSSOFailureReason | null = session\n ? null\n : answer?.kind === 'no_session'\n ? answer.reason\n : query.isError\n ? 'request_failed'\n : null;\n const signedOut = answer?.kind === 'no_session' && answer.signedOut === true;\n\n useEffect(() => {\n if (!revalidateOnWindowFocus || typeof window === 'undefined') {\n return;\n }\n\n // A revalidation joins a read already in flight (a renewal, the first\n // read) instead of cancelling it.\n const revalidate = () => {\n void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });\n };\n const handleVisibilityChange = () => {\n if (document.visibilityState === 'visible') {\n revalidate();\n }\n };\n\n window.addEventListener('focus', revalidate);\n document.addEventListener('visibilitychange', handleVisibilityChange);\n\n return () => {\n window.removeEventListener('focus', revalidate);\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n };\n }, [revalidateOnWindowFocus, queryClient, queryKey]);\n\n // Redirect to SSO login when the endpoint answered no session and no error\n // UI was provided. Signing out redirects by itself, when asked to.\n useEffect(() => {\n if (\n !loading &&\n reason === 'unauthenticated' &&\n !signedOut &&\n !displayOnError &&\n typeof window !== 'undefined'\n ) {\n window.location.assign(ssoUrls.login);\n }\n }, [loading, reason, signedOut, displayOnError, ssoUrls.login]);\n\n const retry = useCallback(() => {\n void queryClient.resetQueries({ queryKey });\n }, [queryClient, queryKey]);\n\n const logout = useCallback(async (redirectOnLogout = true) => {\n await queryClient.cancelQueries({ queryKey });\n\n try {\n await fetch(ssoUrls.logout, {\n method: 'POST',\n credentials: 'include',\n });\n } catch (logoutError) {\n console.error('[AthenaSDK] Failed to invalidate SSO session during logout', logoutError);\n }\n\n queryClient.setQueryData<SessionAnswer>(queryKey, {\n kind: 'no_session',\n reason: 'unauthenticated',\n signedOut: true,\n });\n\n if (redirectOnLogout && typeof window !== 'undefined') {\n window.location.assign(ssoUrls.login);\n }\n }, [queryClient, queryKey, ssoUrls.login, ssoUrls.logout]);\n\n // The SSO side of the SDK's credential source: `force` (the previous token\n // was refused) reads the session again for a new token, joining a read\n // already in flight; otherwise, or when that read has no session to hand\n // back, the held one is served.\n const sessionRef = useRef(session);\n sessionRef.current = session;\n const getToken = useCallback<AthenaStatewireTokenGetter>(\n async ({ force = false } = {}) => {\n if (force) {\n try {\n const renewed = await queryClient.fetchQuery({\n queryKey,\n queryFn,\n staleTime: 0,\n retry: false,\n networkMode: 'always',\n });\n if (renewed.kind === 'session') return renewed.data.accessToken;\n } catch {\n // The read gave up (already reported) or was cancelled.\n }\n }\n return sessionRef.current?.accessToken ?? null;\n },\n [queryClient, queryKey, queryFn],\n );\n\n const authState: AthenaAuthState = useMemo(() => {\n if (loading) {\n return {\n isLoggedIn: false,\n isLoading: true,\n user: null,\n accessToken: null,\n orgs: [],\n logout: async () => {},\n getToken,\n };\n }\n\n if (!session) {\n return {\n isLoggedIn: false,\n isLoading: false,\n user: null,\n accessToken: null,\n orgs: [],\n logout,\n getToken,\n };\n }\n\n const { user, orgs, accessToken } = mapSSOResponse(session);\n\n return {\n isLoggedIn: true,\n isLoading: false,\n user,\n accessToken,\n orgs,\n logout,\n getToken,\n };\n }, [loading, session, logout, getToken]);\n\n if (authState.isLoading) {\n return <>{displayWhileLoading ?? null}</>;\n }\n\n if (!authState.isLoggedIn) {\n if (typeof displayOnError === 'function') {\n return <>{displayOnError({ reason: reason ?? 'unauthenticated', retry })}</>;\n }\n return displayOnError ? <>{displayOnError}</> : null;\n }\n\n return (\n <AthenaAuthContext.Provider value={authState}>\n {children}\n </AthenaAuthContext.Provider>\n );\n}\n"],"names":["DEFAULT_MINT_RETRY_POLICY","isRetryableMintStatus","getMintRetryDelay","athenaDiagnostics","AthenaSdkError","useState","QueryClient","useRef","useMemo","useCallback","useQuery","useEffect","jsx","Fragment","AthenaAuthContext"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AthenaSSOProvider.cjs","sources":["../../src/auth/AthenaSSOProvider.tsx"],"sourcesContent":["import { QueryClient, useQuery } from '@tanstack/react-query';\nimport type { ReactNode } from 'react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { AthenaSdkError } from '../diagnostics/errors';\nimport {\n DEFAULT_MINT_RETRY_POLICY,\n getMintRetryDelay,\n isRetryableMintStatus,\n type MintRetryPolicy,\n} from '../runtime/auth';\nimport type { AthenaStatewireTokenGetter } from '../runtime/statewire-auth-headers';\nimport { AthenaAuthContext } from './AthenaAuthContext';\nimport {\n canSignBackIn,\n redirectToSignIn,\n reportSessionEnded,\n restoreSignBackInHash,\n signBackIn,\n} from './sign-back-in';\nimport type {\n AthenaAuthState,\n AthenaOrg,\n AthenaSSOUserInfo,\n AthenaUser,\n} from './types';\n\n// ─── Props ────────────────────────────────────────────────────────────\n\nexport interface AthenaSSOProviderProps {\n children: ReactNode;\n\n /**\n * Base URL used to resolve the Athena SSO endpoints.\n *\n * When omitted, the provider defaults to same-origin relative paths:\n * `/api/sso/userinfo`, `/api/sso/initiate`, and `/api/sso/logout`.\n *\n * Use this when your frontend and backend live on different origins\n * (for example `https://app.example.com` + `https://api.example.com`).\n */\n ssoBaseUrl?: string;\n\n /**\n * Optional override for the SSO user-info endpoint that returns the\n * authenticated user's data and access token.\n *\n * The endpoint must return JSON matching `AthenaSSOUserInfo`:\n * ```json\n * {\n * \"user\": { \"user_id\": \"…\", \"email\": \"…\", … },\n * \"orgMemberInfos\": [{ \"orgId\": \"…\", \"orgName\": \"…\", \"urlSafeOrgName\": \"…\" }],\n * \"accessToken\": \"…\"\n * }\n * ```\n *\n * The request is sent with `credentials: 'include'` so that\n * session cookies are forwarded automatically.\n */\n ssoUserInfoUrl?: string;\n\n /**\n * Optional override for the URL to redirect the user to when no valid\n * SSO session exists.\n * This is typically the SSO initiation endpoint on your backend\n * (e.g. `https://api.yourdomain.com/api/sso/initiate`).\n */\n ssoLoginUrl?: string;\n\n /**\n * Optional override for the backend SSO logout endpoint.\n * Defaults to `/api/sso/logout` (or `${ssoBaseUrl}/api/sso/logout`).\n */\n ssoLogoutUrl?: string;\n\n /**\n * Custom element displayed while the SSO session is being verified.\n * Defaults to `null` (renders nothing).\n */\n displayWhileLoading?: ReactNode;\n\n /**\n * Shown when there is no session to sign the app in with: an element, or a\n * function that receives why (`reason`) and a `retry`, so the app can offer\n * sign-in only to a person who is signed out and a retry when the endpoint\n * could not answer (see `AthenaSSOFailure`).\n * If not provided, the user is automatically redirected to `ssoLoginUrl`\n * (with `return_to` set to the page) when the endpoint answers that there is\n * no session, at most once per tab every two minutes: within that time after\n * a trip through sign-in, the provider renders nothing instead of looping.\n *\n * A session that ends while the page is open goes through sign-in on its own\n * instead (`signBackInOnSessionEnd`); this shows only when that trip is\n * off, did not help, or cannot help (the endpoint answered 403).\n */\n displayOnError?: ReactNode | ((failure: AthenaSSOFailure) => ReactNode);\n\n /**\n * When a session this provider served ends while the page is open (a later\n * read answers 401: the person signed out of Athena elsewhere, or the host's\n * session for this app expired), send the page through `ssoLoginUrl` once,\n * with `return_to` set to the page, instead of showing `displayOnError`. A\n * 403 is the host refusing this person the app (Marathon's `not_authorized`\n * or `unknown_asset`): signing in again cannot change that, so it shows\n * `displayOnError`. A person still signed in to Athena comes straight back to\n * the same page, signed in again; one who is not sees their sign-in page\n * first. The app stays on screen until the page leaves, and an\n * `AthenaProvider` inside keeps the composer's draft for the page that\n * returns. At most one trip per tab every two minutes: a second end within\n * that time shows `displayOnError`. Defaults to `true`.\n */\n signBackInOnSessionEnd?: boolean;\n\n /**\n * Called with the typed error (`auth_sso_failed`) when a session read gives\n * up. A nested `AthenaProvider`'s `onError` is not mounted while the first\n * read runs, so observe sign-in failures here.\n */\n onError?: (error: AthenaSdkError) => void;\n\n /**\n * Re-validate the SSO session when the window regains focus.\n * Disabled by default. When enabled, token expiry and user switching\n * are picked up without requiring a full page reload.\n */\n revalidateOnWindowFocus?: boolean;\n\n /** @internal Overrides how a session read retries; used by tests. */\n retryPolicy?: MintRetryPolicy;\n}\n\n/**\n * Why the provider has no session.\n *\n * - `unauthenticated`: the endpoint answered 401 or 403, or the person signed\n * out. Offer sign-in.\n * - `request_failed`: the endpoint did not answer usably. A network error, a\n * timeout, or a 5xx / 408 / 429 on every attempt of one read, or another\n * status. The person may well be signed in. Offer `retry`.\n * - `invalid_response`: it answered without a usable session.\n */\nexport type AthenaSSOFailureReason = 'unauthenticated' | 'invalid_response' | 'request_failed';\n\n/** What `displayOnError` receives when it is a function. */\nexport interface AthenaSSOFailure {\n reason: AthenaSSOFailureReason;\n /** Read the session again. `displayWhileLoading` shows meanwhile. */\n retry: () => void;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────\n\n/**\n * How one session read retries: the parent bridge's mint policy (Marathon's\n * `/api/sso/userinfo` and `/_athena/auth` mint the same credential), with a\n * longer wait per request, since every request mints and a slow answer is\n * still an answer.\n */\nconst SSO_USERINFO_RETRY_POLICY: MintRetryPolicy = {\n ...DEFAULT_MINT_RETRY_POLICY,\n timeoutMs: 10_000,\n};\n\n/** One userinfo request, classified. */\ntype UserInfoAttempt =\n | { kind: 'session'; data: AthenaSSOUserInfo }\n | { kind: 'unauthenticated'; status: 401 | 403 }\n | { kind: 'invalid_response' }\n /** A status it does not retry, or a 2xx whose body is not JSON. */\n | { kind: 'failed'; status: number }\n | {\n kind: 'transient';\n status: number | null;\n reason: 'status' | 'network' | 'timeout';\n cause?: unknown;\n };\n\nconst abortError = () => new DOMException('The session read was cancelled.', 'AbortError');\n\n/** One userinfo request. Throws an `AbortError` when `signal` cancels the read. */\nasync function readUserInfoOnce({\n url,\n signal,\n timeoutMs,\n}: {\n url: string;\n signal: AbortSignal;\n timeoutMs: number;\n}): Promise<UserInfoAttempt> {\n if (signal.aborted) throw abortError();\n const request = new AbortController();\n const cancel = () => request.abort();\n signal.addEventListener('abort', cancel, { once: true });\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n request.abort();\n }, timeoutMs);\n\n try {\n const response = await fetch(url, { credentials: 'include', signal: request.signal });\n if (response.status === 401 || response.status === 403) {\n return { kind: 'unauthenticated', status: response.status === 401 ? 401 : 403 };\n }\n if (!response.ok) {\n return isRetryableMintStatus(response.status)\n ? { kind: 'transient', status: response.status, reason: 'status' }\n : { kind: 'failed', status: response.status };\n }\n\n let data: unknown;\n try {\n data = await response.json();\n } catch (error) {\n if (signal.aborted) throw abortError();\n return timedOut\n ? { kind: 'transient', status: null, reason: 'timeout', cause: error }\n : { kind: 'failed', status: response.status };\n }\n return isValidSSOUserInfo(data) ? { kind: 'session', data } : { kind: 'invalid_response' };\n } catch (error) {\n if (signal.aborted) throw abortError();\n return { kind: 'transient', status: null, reason: timedOut ? 'timeout' : 'network', cause: error };\n } finally {\n clearTimeout(timer);\n signal.removeEventListener('abort', cancel);\n }\n}\n\n/**\n * What a read concluded, as the query holds it. A read that could not answer\n * throws instead (`request_failed`), so an earlier answer stays.\n */\ntype SessionAnswer =\n | { kind: 'session'; data: AthenaSSOUserInfo }\n | {\n kind: 'no_session';\n reason: Exclude<AthenaSSOFailureReason, 'request_failed'>;\n /**\n * What the endpoint answered when it said no session: 401, there is\n * none; 403, the host refused this person the app. Absent otherwise.\n */\n status?: 401 | 403;\n /** The person signed out here (`logout`), rather than the endpoint answering no session. */\n signedOut?: true;\n };\n\n/**\n * One session read: one bounded cycle of userinfo requests. A transient\n * failure is retried with `policy`'s capped, jittered backoff; a read that\n * gives up throws the typed error it reports. Throws an `AbortError` when\n * `signal` cancels the read.\n */\nasync function readSession({\n url,\n signal,\n policy,\n}: {\n url: string;\n signal: AbortSignal;\n policy: MintRetryPolicy;\n}): Promise<SessionAnswer> {\n const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));\n for (let attempts = 1; ; attempts += 1) {\n const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });\n\n if (attempt.kind === 'session') return { kind: 'session', data: attempt.data };\n if (attempt.kind === 'unauthenticated') {\n return { kind: 'no_session', reason: 'unauthenticated', status: attempt.status };\n }\n if (attempt.kind === 'invalid_response') {\n console.error(\n '[AthenaSDK] Invalid SSO userinfo response: expected \"user.user_id\", \"user.email\", \"orgMemberInfos\", and \"accessToken\"',\n );\n return { kind: 'no_session', reason: 'invalid_response' };\n }\n\n if (attempt.kind === 'transient' && attempts < maxAttempts) {\n const delayMs = getMintRetryDelay({ retry: attempts, policy });\n athenaDiagnostics.emit('auth.sso.userinfo.retry', {\n level: 'warn',\n data: {\n status: attempt.status,\n reason: attempt.reason,\n attempt: attempts,\n max_attempts: maxAttempts,\n delay_ms: delayMs,\n },\n });\n await waitBeforeRetry({ delayMs, signal });\n continue;\n }\n\n throw sessionReadError({ url, attempt, attempts });\n }\n}\n\n/** The backoff between two requests of one read; cancelling the read ends it at once. */\nfunction waitBeforeRetry({ delayMs, signal }: { delayMs: number; signal: AbortSignal }): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal.aborted) {\n reject(abortError());\n return;\n }\n const onAbort = () => {\n clearTimeout(timer);\n reject(abortError());\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, delayMs);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** The typed error a read reports when it gives up without an answer about the session. */\nfunction sessionReadError({\n url,\n attempt,\n attempts,\n}: {\n url: string;\n attempt: Extract<UserInfoAttempt, { kind: 'failed' | 'transient' }>;\n attempts: number;\n}): AthenaSdkError {\n if (attempt.kind === 'failed') {\n const unreadable = attempt.status >= 200 && attempt.status < 300;\n return new AthenaSdkError({\n code: 'auth_sso_failed',\n message: unreadable\n ? `The SSO session read (${url}) answered HTTP ${attempt.status} without a JSON body.`\n : `The SSO session read (${url}) answered HTTP ${attempt.status}.`,\n hint: 'Check that ssoBaseUrl / ssoUserInfoUrl point at the SSO userinfo endpoint.',\n status: attempt.status,\n context: { attempts, reason: unreadable ? 'invalid_body' : 'status' },\n });\n }\n const answer =\n attempt.status !== null\n ? `HTTP ${attempt.status}`\n : attempt.reason === 'timeout'\n ? 'a timeout'\n : 'a network error';\n return new AthenaSdkError({\n code: 'auth_sso_failed',\n message: `The SSO session read (${url}) failed ${attempts} time${attempts === 1 ? '' : 's'} (last: ${answer}).`,\n hint: 'The sign-in service did not answer; the person may still be signed in. Offer a retry (displayOnError receives retry).',\n ...(attempt.status !== null && { status: attempt.status }),\n ...(attempt.cause !== undefined && { cause: attempt.cause }),\n context: { attempts, reason: attempt.reason },\n });\n}\n\nconst DEFAULT_SSO_PATHS = {\n userInfo: '/api/sso/userinfo',\n login: '/api/sso/initiate',\n logout: '/api/sso/logout',\n} as const;\n\nconst trimTrailingSlash = (value: string): string => value.replace(/\\/+$/, '');\n\nfunction resolveSSOUrl({\n baseUrl,\n overrideUrl,\n defaultPath,\n}: {\n baseUrl?: string;\n overrideUrl?: string;\n defaultPath: string;\n}): string {\n if (overrideUrl) {\n return overrideUrl;\n }\n\n if (!baseUrl) {\n return defaultPath;\n }\n\n return `${trimTrailingSlash(baseUrl)}${defaultPath}`;\n}\n\nfunction isValidSSOUserInfo(data: unknown): data is AthenaSSOUserInfo {\n if (!data || typeof data !== 'object') {\n return false;\n }\n\n const candidate = data as {\n accessToken?: unknown;\n orgMemberInfos?: unknown;\n user?: {\n user_id?: unknown;\n email?: unknown;\n };\n };\n\n return (\n typeof candidate.accessToken === 'string' &&\n Array.isArray(candidate.orgMemberInfos) &&\n !!candidate.user &&\n typeof candidate.user.user_id === 'string' &&\n typeof candidate.user.email === 'string'\n );\n}\n\n/** Map the backend SSO response to the SDK's user/org types. */\nfunction mapSSOResponse(data: AthenaSSOUserInfo): {\n user: AthenaUser;\n orgs: AthenaOrg[];\n accessToken: string;\n} {\n const user: AthenaUser = {\n userId: data.user.user_id,\n email: data.user.email,\n firstName: data.user.first_name || undefined,\n lastName: data.user.last_name || undefined,\n username: data.user.username || undefined,\n pictureUrl: data.user.picture_url || undefined,\n properties: data.user.properties,\n };\n\n const orgs: AthenaOrg[] = data.orgMemberInfos.map((org) => ({\n orgId: org.orgId,\n orgName: org.orgName,\n urlSafeOrgName: org.urlSafeOrgName,\n }));\n\n return { user, orgs, accessToken: data.accessToken };\n}\n\n// ─── Provider ─────────────────────────────────────────────────────────\n\n/**\n * Wraps your app with Athena authentication powered by an external SSO\n * identity provider (e.g. Microsoft Entra / Azure AD, Okta, etc.).\n *\n * The SSO login flow is handled entirely by your backend. This provider\n * verifies the session by fetching `ssoUserInfoUrl` and exposes the\n * same `AthenaAuthState` context that `AthenaAuthProvider` (PropelAuth)\n * provides, so downstream components like `<AthenaProvider>` and\n * `useAthenaAuth()` work identically.\n *\n * Place this **above** `<AthenaProvider>` in the component tree.\n *\n * @example\n * ```tsx\n * import { AthenaSSOProvider } from '@athenaintel/react/auth';\n * import { AthenaProvider, AthenaChat } from '@athenaintel/react';\n *\n * function App() {\n * return (\n * <AthenaSSOProvider\n * ssoUserInfoUrl=\"https://api.yourdomain.com/api/sso/userinfo\"\n * ssoLoginUrl=\"https://api.yourdomain.com/api/sso/initiate\"\n * >\n * <AthenaProvider>\n * <AthenaChat />\n * </AthenaProvider>\n * </AthenaSSOProvider>\n * );\n * }\n * ```\n */\nexport function AthenaSSOProvider({\n children,\n ssoBaseUrl,\n ssoUserInfoUrl,\n ssoLoginUrl,\n ssoLogoutUrl,\n displayWhileLoading,\n displayOnError,\n onError,\n revalidateOnWindowFocus = false,\n signBackInOnSessionEnd = true,\n retryPolicy = SSO_USERINFO_RETRY_POLICY,\n}: AthenaSSOProviderProps) {\n // The provider's own client, never a host's: the session holds an access\n // token, and a host may persist its query cache.\n const [queryClient] = useState(() => new QueryClient());\n const retryPolicyRef = useRef(retryPolicy);\n retryPolicyRef.current = retryPolicy;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n\n const ssoUrls = useMemo(\n () => ({\n userInfo: resolveSSOUrl({\n baseUrl: ssoBaseUrl,\n overrideUrl: ssoUserInfoUrl,\n defaultPath: DEFAULT_SSO_PATHS.userInfo,\n }),\n login: resolveSSOUrl({\n baseUrl: ssoBaseUrl,\n overrideUrl: ssoLoginUrl,\n defaultPath: DEFAULT_SSO_PATHS.login,\n }),\n logout: resolveSSOUrl({\n baseUrl: ssoBaseUrl,\n overrideUrl: ssoLogoutUrl,\n defaultPath: DEFAULT_SSO_PATHS.logout,\n }),\n }),\n [ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl],\n );\n\n const queryKey = useMemo(() => ['athena-sdk', 'sso-session', ssoUrls.userInfo], [ssoUrls.userInfo]);\n\n // One read is one bounded cycle (`readSession`). A read that gives up\n // throws, so the query keeps the session an earlier read answered: a failed\n // background revalidation or renewal does not sign the person out.\n const queryFn = useCallback(\n async ({ signal }: { signal: AbortSignal }): Promise<SessionAnswer> => {\n try {\n return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });\n } catch (readError) {\n if (readError instanceof AthenaSdkError) {\n athenaDiagnostics.emit('auth.sso.userinfo.error', { error: readError });\n try {\n onErrorRef.current?.(readError);\n } catch (callbackError) {\n console.error('[AthenaSDK] AthenaSSOProvider onError threw', callbackError);\n }\n }\n throw readError;\n }\n },\n [ssoUrls.userInfo],\n );\n\n const query = useQuery(\n {\n queryKey,\n queryFn,\n // `readSession` retries a transient failure itself, with the bridge's\n // policy; the query never retries on top of it, and reads while the\n // browser reports itself offline too, so that policy decides.\n retry: false,\n networkMode: 'always',\n staleTime: Number.POSITIVE_INFINITY,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n },\n queryClient,\n );\n\n const answer = query.data;\n const session = answer?.kind === 'session' ? answer.data : null;\n const loading = query.isPending;\n const reason: AthenaSSOFailureReason | null = session\n ? null\n : answer?.kind === 'no_session'\n ? answer.reason\n : query.isError\n ? 'request_failed'\n : null;\n const signedOut = answer?.kind === 'no_session' && answer.signedOut === true;\n // What the endpoint answered when it said no session (401 or 403).\n const answerStatus = answer?.kind === 'no_session' ? answer.status : undefined;\n\n // The last session this provider served on this page. A no-session answer\n // after one means the session ended while the page was open. Refs, not\n // state: both follow the query's answer, and a state update here would\n // render the ending once with `displayOnError` before deciding.\n const servedSessionRef = useRef<AthenaSSOUserInfo | null>(null);\n if (session !== null) servedSessionRef.current = session;\n const servedSession = servedSessionRef.current;\n // When the read that ended the session answered. Every read moves it, even\n // one whose answer equals the last (the query keeps the equal answer's\n // object), so each ending is told apart from the one before.\n const endedAt: number | null =\n !loading && servedSession !== null && reason === 'unauthenticated' && !signedOut\n ? query.dataUpdatedAt\n : null;\n // Decided once per ending: a trip through sign-in, unless it is off, the\n // endpoint answered 403 (the host refused this person the app: signing in\n // again cannot change that), or one less than two minutes ago did not bring\n // a session back (`canSignBackIn`).\n const endingRef = useRef<{ at: number; signBackIn: boolean } | null>(null);\n if (endedAt !== null && endingRef.current?.at !== endedAt) {\n endingRef.current = {\n at: endedAt,\n signBackIn: signBackInOnSessionEnd && answerStatus === 401 && canSignBackIn(),\n };\n }\n // An ending whose trip could not start after all (storage refused the guard).\n const [unsentEnding, setUnsentEnding] = useState<number | null>(null);\n const signingBackIn =\n endedAt !== null &&\n endingRef.current?.at === endedAt &&\n endingRef.current.signBackIn &&\n unsentEnding !== endedAt;\n\n useEffect(() => {\n if (endedAt === null) return;\n if (!signingBackIn) {\n // An ending the page stays on (`displayOnError` shows it). One whose\n // trip could not start was reported by `signBackIn` already.\n if (unsentEnding !== endedAt) {\n reportSessionEnded({ source: 'sso', status: answerStatus, signBackIn: false });\n }\n return;\n }\n // The app is still mounted (it keeps the last session until the page\n // leaves), so what it keeps for the page that returns, the composer's\n // draft among it, is kept on the way out.\n if (!signBackIn({ loginUrl: ssoUrls.login, source: 'sso', status: answerStatus })) {\n setUnsentEnding(endedAt);\n }\n }, [endedAt, signingBackIn, unsentEnding, ssoUrls.login, answerStatus]);\n\n // The page a trip through sign-in returned to gets its URL fragment back\n // (a hash-routed app's route), which Marathon's return drops.\n useEffect(() => {\n restoreSignBackInHash();\n }, []);\n\n useEffect(() => {\n if (!revalidateOnWindowFocus || typeof window === 'undefined') {\n return;\n }\n\n // A revalidation joins a read already in flight (a renewal, the first\n // read) instead of cancelling it.\n const revalidate = () => {\n void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });\n };\n const handleVisibilityChange = () => {\n if (document.visibilityState === 'visible') {\n revalidate();\n }\n };\n\n window.addEventListener('focus', revalidate);\n document.addEventListener('visibilitychange', handleVisibilityChange);\n\n return () => {\n window.removeEventListener('focus', revalidate);\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n };\n }, [revalidateOnWindowFocus, queryClient, queryKey]);\n\n // Without error UI, send the page to SSO login, returning to it, whenever\n // the endpoint answered no session: a first read with none, or an ending\n // that does not go through `signBackIn` above. At most once per tab every\n // two minutes (`redirectToSignIn` keeps `signBackIn`'s guard): after a trip\n // that did not bring a session back, the page stays and renders nothing\n // instead of looping. Signing out redirects by itself, when asked to.\n useEffect(() => {\n if (\n loading ||\n reason !== 'unauthenticated' ||\n signedOut ||\n signingBackIn ||\n displayOnError ||\n typeof window === 'undefined'\n ) {\n return;\n }\n if (!redirectToSignIn({ loginUrl: ssoUrls.login })) {\n console.warn(\n '[AthenaSDK] Not sending the page to sign-in again: this tab went through sign-in less than two minutes ago and still has no session. Pass `displayOnError` to show a sign-in screen instead.',\n );\n }\n }, [loading, reason, signedOut, signingBackIn, displayOnError, ssoUrls.login]);\n\n const retry = useCallback(() => {\n void queryClient.resetQueries({ queryKey });\n }, [queryClient, queryKey]);\n\n const logout = useCallback(async (redirectOnLogout = true) => {\n await queryClient.cancelQueries({ queryKey });\n\n try {\n await fetch(ssoUrls.logout, {\n method: 'POST',\n credentials: 'include',\n });\n } catch (logoutError) {\n console.error('[AthenaSDK] Failed to invalidate SSO session during logout', logoutError);\n }\n\n queryClient.setQueryData<SessionAnswer>(queryKey, {\n kind: 'no_session',\n reason: 'unauthenticated',\n signedOut: true,\n });\n\n if (redirectOnLogout && typeof window !== 'undefined') {\n window.location.assign(ssoUrls.login);\n }\n }, [queryClient, queryKey, ssoUrls.login, ssoUrls.logout]);\n\n // The SSO side of the SDK's credential source: `force` (the previous token\n // was refused) reads the session again for a new token, joining a read\n // already in flight; otherwise, or when that read has no session to hand\n // back, the held one is served.\n const sessionRef = useRef(session);\n sessionRef.current = session;\n const getToken = useCallback<AthenaStatewireTokenGetter>(\n async ({ force = false } = {}) => {\n if (force) {\n try {\n const renewed = await queryClient.fetchQuery({\n queryKey,\n queryFn,\n staleTime: 0,\n retry: false,\n networkMode: 'always',\n });\n if (renewed.kind === 'session') return renewed.data.accessToken;\n } catch {\n // The read gave up (already reported) or was cancelled.\n }\n }\n return sessionRef.current?.accessToken ?? null;\n },\n [queryClient, queryKey, queryFn],\n );\n\n // While the page is on its way through sign-in, the app keeps the session it\n // had: it stays on screen instead of flashing `displayOnError`.\n const shownSession = session ?? (signingBackIn ? servedSession : null);\n const authState: AthenaAuthState = useMemo(() => {\n if (loading) {\n return {\n isLoggedIn: false,\n isLoading: true,\n user: null,\n accessToken: null,\n orgs: [],\n logout: async () => {},\n getToken,\n };\n }\n\n if (!shownSession) {\n return {\n isLoggedIn: false,\n isLoading: false,\n user: null,\n accessToken: null,\n orgs: [],\n logout,\n getToken,\n };\n }\n\n const { user, orgs, accessToken } = mapSSOResponse(shownSession);\n\n return {\n isLoggedIn: true,\n isLoading: false,\n user,\n accessToken,\n orgs,\n logout,\n getToken,\n };\n }, [loading, shownSession, logout, getToken]);\n\n if (authState.isLoading) {\n return <>{displayWhileLoading ?? null}</>;\n }\n\n if (!authState.isLoggedIn) {\n if (typeof displayOnError === 'function') {\n return <>{displayOnError({ reason: reason ?? 'unauthenticated', retry })}</>;\n }\n return displayOnError ? <>{displayOnError}</> : null;\n }\n\n return (\n <AthenaAuthContext.Provider value={authState}>\n {children}\n </AthenaAuthContext.Provider>\n );\n}\n"],"names":["DEFAULT_MINT_RETRY_POLICY","isRetryableMintStatus","getMintRetryDelay","athenaDiagnostics","AthenaSdkError","useState","QueryClient","useRef","useMemo","useCallback","_a","useQuery","canSignBackIn","useEffect","reportSessionEnded","signBackIn","restoreSignBackInHash","redirectToSignIn","jsx","Fragment","AthenaAuthContext"],"mappings":"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"}
|
|
@@ -53,9 +53,30 @@ export interface AthenaSSOProviderProps {
|
|
|
53
53
|
* sign-in only to a person who is signed out and a retry when the endpoint
|
|
54
54
|
* could not answer (see `AthenaSSOFailure`).
|
|
55
55
|
* If not provided, the user is automatically redirected to `ssoLoginUrl`
|
|
56
|
-
* when the endpoint answers that there is
|
|
56
|
+
* (with `return_to` set to the page) when the endpoint answers that there is
|
|
57
|
+
* no session, at most once per tab every two minutes: within that time after
|
|
58
|
+
* a trip through sign-in, the provider renders nothing instead of looping.
|
|
59
|
+
*
|
|
60
|
+
* A session that ends while the page is open goes through sign-in on its own
|
|
61
|
+
* instead (`signBackInOnSessionEnd`); this shows only when that trip is
|
|
62
|
+
* off, did not help, or cannot help (the endpoint answered 403).
|
|
57
63
|
*/
|
|
58
64
|
displayOnError?: ReactNode | ((failure: AthenaSSOFailure) => ReactNode);
|
|
65
|
+
/**
|
|
66
|
+
* When a session this provider served ends while the page is open (a later
|
|
67
|
+
* read answers 401: the person signed out of Athena elsewhere, or the host's
|
|
68
|
+
* session for this app expired), send the page through `ssoLoginUrl` once,
|
|
69
|
+
* with `return_to` set to the page, instead of showing `displayOnError`. A
|
|
70
|
+
* 403 is the host refusing this person the app (Marathon's `not_authorized`
|
|
71
|
+
* or `unknown_asset`): signing in again cannot change that, so it shows
|
|
72
|
+
* `displayOnError`. A person still signed in to Athena comes straight back to
|
|
73
|
+
* the same page, signed in again; one who is not sees their sign-in page
|
|
74
|
+
* first. The app stays on screen until the page leaves, and an
|
|
75
|
+
* `AthenaProvider` inside keeps the composer's draft for the page that
|
|
76
|
+
* returns. At most one trip per tab every two minutes: a second end within
|
|
77
|
+
* that time shows `displayOnError`. Defaults to `true`.
|
|
78
|
+
*/
|
|
79
|
+
signBackInOnSessionEnd?: boolean;
|
|
59
80
|
/**
|
|
60
81
|
* Called with the typed error (`auth_sso_failed`) when a session read gives
|
|
61
82
|
* up. A nested `AthenaProvider`'s `onError` is not mounted while the first
|
|
@@ -119,4 +140,4 @@ export interface AthenaSSOFailure {
|
|
|
119
140
|
* }
|
|
120
141
|
* ```
|
|
121
142
|
*/
|
|
122
|
-
export declare function AthenaSSOProvider({ children, ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl, displayWhileLoading, displayOnError, onError, revalidateOnWindowFocus, retryPolicy, }: AthenaSSOProviderProps): import("react").JSX.Element | null;
|
|
143
|
+
export declare function AthenaSSOProvider({ children, ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl, displayWhileLoading, displayOnError, onError, revalidateOnWindowFocus, signBackInOnSessionEnd, retryPolicy, }: AthenaSSOProviderProps): import("react").JSX.Element | null;
|
|
@@ -5,6 +5,7 @@ import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
|
|
|
5
5
|
import { AthenaSdkError } from "../diagnostics/errors.js";
|
|
6
6
|
import { DEFAULT_MINT_RETRY_POLICY, getMintRetryDelay, isRetryableMintStatus } from "../runtime/auth.js";
|
|
7
7
|
import { AthenaAuthContext } from "./AthenaAuthContext.js";
|
|
8
|
+
import { canSignBackIn, reportSessionEnded, signBackIn, restoreSignBackInHash, redirectToSignIn } from "./sign-back-in.js";
|
|
8
9
|
const SSO_USERINFO_RETRY_POLICY = {
|
|
9
10
|
...DEFAULT_MINT_RETRY_POLICY,
|
|
10
11
|
timeoutMs: 1e4
|
|
@@ -26,7 +27,9 @@ async function readUserInfoOnce({
|
|
|
26
27
|
}, timeoutMs);
|
|
27
28
|
try {
|
|
28
29
|
const response = await fetch(url, { credentials: "include", signal: request.signal });
|
|
29
|
-
if (response.status === 401 || response.status === 403)
|
|
30
|
+
if (response.status === 401 || response.status === 403) {
|
|
31
|
+
return { kind: "unauthenticated", status: response.status === 401 ? 401 : 403 };
|
|
32
|
+
}
|
|
30
33
|
if (!response.ok) {
|
|
31
34
|
return isRetryableMintStatus(response.status) ? { kind: "transient", status: response.status, reason: "status" } : { kind: "failed", status: response.status };
|
|
32
35
|
}
|
|
@@ -55,7 +58,9 @@ async function readSession({
|
|
|
55
58
|
for (let attempts = 1; ; attempts += 1) {
|
|
56
59
|
const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });
|
|
57
60
|
if (attempt.kind === "session") return { kind: "session", data: attempt.data };
|
|
58
|
-
if (attempt.kind === "unauthenticated")
|
|
61
|
+
if (attempt.kind === "unauthenticated") {
|
|
62
|
+
return { kind: "no_session", reason: "unauthenticated", status: attempt.status };
|
|
63
|
+
}
|
|
59
64
|
if (attempt.kind === "invalid_response") {
|
|
60
65
|
console.error(
|
|
61
66
|
'[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
|
|
@@ -175,8 +180,10 @@ function AthenaSSOProvider({
|
|
|
175
180
|
displayOnError,
|
|
176
181
|
onError,
|
|
177
182
|
revalidateOnWindowFocus = false,
|
|
183
|
+
signBackInOnSessionEnd = true,
|
|
178
184
|
retryPolicy = SSO_USERINFO_RETRY_POLICY
|
|
179
185
|
}) {
|
|
186
|
+
var _a, _b;
|
|
180
187
|
const [queryClient] = useState(() => new QueryClient());
|
|
181
188
|
const retryPolicyRef = useRef(retryPolicy);
|
|
182
189
|
retryPolicyRef.current = retryPolicy;
|
|
@@ -205,14 +212,14 @@ function AthenaSSOProvider({
|
|
|
205
212
|
const queryKey = useMemo(() => ["athena-sdk", "sso-session", ssoUrls.userInfo], [ssoUrls.userInfo]);
|
|
206
213
|
const queryFn = useCallback(
|
|
207
214
|
async ({ signal }) => {
|
|
208
|
-
var
|
|
215
|
+
var _a2;
|
|
209
216
|
try {
|
|
210
217
|
return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });
|
|
211
218
|
} catch (readError) {
|
|
212
219
|
if (readError instanceof AthenaSdkError) {
|
|
213
220
|
athenaDiagnostics.emit("auth.sso.userinfo.error", { error: readError });
|
|
214
221
|
try {
|
|
215
|
-
(
|
|
222
|
+
(_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, readError);
|
|
216
223
|
} catch (callbackError) {
|
|
217
224
|
console.error("[AthenaSDK] AthenaSSOProvider onError threw", callbackError);
|
|
218
225
|
}
|
|
@@ -242,6 +249,35 @@ function AthenaSSOProvider({
|
|
|
242
249
|
const loading = query.isPending;
|
|
243
250
|
const reason = session ? null : (answer == null ? void 0 : answer.kind) === "no_session" ? answer.reason : query.isError ? "request_failed" : null;
|
|
244
251
|
const signedOut = (answer == null ? void 0 : answer.kind) === "no_session" && answer.signedOut === true;
|
|
252
|
+
const answerStatus = (answer == null ? void 0 : answer.kind) === "no_session" ? answer.status : void 0;
|
|
253
|
+
const servedSessionRef = useRef(null);
|
|
254
|
+
if (session !== null) servedSessionRef.current = session;
|
|
255
|
+
const servedSession = servedSessionRef.current;
|
|
256
|
+
const endedAt = !loading && servedSession !== null && reason === "unauthenticated" && !signedOut ? query.dataUpdatedAt : null;
|
|
257
|
+
const endingRef = useRef(null);
|
|
258
|
+
if (endedAt !== null && ((_a = endingRef.current) == null ? void 0 : _a.at) !== endedAt) {
|
|
259
|
+
endingRef.current = {
|
|
260
|
+
at: endedAt,
|
|
261
|
+
signBackIn: signBackInOnSessionEnd && answerStatus === 401 && canSignBackIn()
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const [unsentEnding, setUnsentEnding] = useState(null);
|
|
265
|
+
const signingBackIn = endedAt !== null && ((_b = endingRef.current) == null ? void 0 : _b.at) === endedAt && endingRef.current.signBackIn && unsentEnding !== endedAt;
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (endedAt === null) return;
|
|
268
|
+
if (!signingBackIn) {
|
|
269
|
+
if (unsentEnding !== endedAt) {
|
|
270
|
+
reportSessionEnded({ source: "sso", status: answerStatus, signBackIn: false });
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!signBackIn({ loginUrl: ssoUrls.login, source: "sso", status: answerStatus })) {
|
|
275
|
+
setUnsentEnding(endedAt);
|
|
276
|
+
}
|
|
277
|
+
}, [endedAt, signingBackIn, unsentEnding, ssoUrls.login, answerStatus]);
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
restoreSignBackInHash();
|
|
280
|
+
}, []);
|
|
245
281
|
useEffect(() => {
|
|
246
282
|
if (!revalidateOnWindowFocus || typeof window === "undefined") {
|
|
247
283
|
return;
|
|
@@ -262,10 +298,15 @@ function AthenaSSOProvider({
|
|
|
262
298
|
};
|
|
263
299
|
}, [revalidateOnWindowFocus, queryClient, queryKey]);
|
|
264
300
|
useEffect(() => {
|
|
265
|
-
if (
|
|
266
|
-
|
|
301
|
+
if (loading || reason !== "unauthenticated" || signedOut || signingBackIn || displayOnError || typeof window === "undefined") {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!redirectToSignIn({ loginUrl: ssoUrls.login })) {
|
|
305
|
+
console.warn(
|
|
306
|
+
"[AthenaSDK] Not sending the page to sign-in again: this tab went through sign-in less than two minutes ago and still has no session. Pass `displayOnError` to show a sign-in screen instead."
|
|
307
|
+
);
|
|
267
308
|
}
|
|
268
|
-
}, [loading, reason, signedOut, displayOnError, ssoUrls.login]);
|
|
309
|
+
}, [loading, reason, signedOut, signingBackIn, displayOnError, ssoUrls.login]);
|
|
269
310
|
const retry = useCallback(() => {
|
|
270
311
|
void queryClient.resetQueries({ queryKey });
|
|
271
312
|
}, [queryClient, queryKey]);
|
|
@@ -292,7 +333,7 @@ function AthenaSSOProvider({
|
|
|
292
333
|
sessionRef.current = session;
|
|
293
334
|
const getToken = useCallback(
|
|
294
335
|
async ({ force = false } = {}) => {
|
|
295
|
-
var
|
|
336
|
+
var _a2;
|
|
296
337
|
if (force) {
|
|
297
338
|
try {
|
|
298
339
|
const renewed = await queryClient.fetchQuery({
|
|
@@ -306,10 +347,11 @@ function AthenaSSOProvider({
|
|
|
306
347
|
} catch {
|
|
307
348
|
}
|
|
308
349
|
}
|
|
309
|
-
return ((
|
|
350
|
+
return ((_a2 = sessionRef.current) == null ? void 0 : _a2.accessToken) ?? null;
|
|
310
351
|
},
|
|
311
352
|
[queryClient, queryKey, queryFn]
|
|
312
353
|
);
|
|
354
|
+
const shownSession = session ?? (signingBackIn ? servedSession : null);
|
|
313
355
|
const authState = useMemo(() => {
|
|
314
356
|
if (loading) {
|
|
315
357
|
return {
|
|
@@ -323,7 +365,7 @@ function AthenaSSOProvider({
|
|
|
323
365
|
getToken
|
|
324
366
|
};
|
|
325
367
|
}
|
|
326
|
-
if (!
|
|
368
|
+
if (!shownSession) {
|
|
327
369
|
return {
|
|
328
370
|
isLoggedIn: false,
|
|
329
371
|
isLoading: false,
|
|
@@ -334,7 +376,7 @@ function AthenaSSOProvider({
|
|
|
334
376
|
getToken
|
|
335
377
|
};
|
|
336
378
|
}
|
|
337
|
-
const { user, orgs, accessToken } = mapSSOResponse(
|
|
379
|
+
const { user, orgs, accessToken } = mapSSOResponse(shownSession);
|
|
338
380
|
return {
|
|
339
381
|
isLoggedIn: true,
|
|
340
382
|
isLoading: false,
|
|
@@ -344,7 +386,7 @@ function AthenaSSOProvider({
|
|
|
344
386
|
logout,
|
|
345
387
|
getToken
|
|
346
388
|
};
|
|
347
|
-
}, [loading,
|
|
389
|
+
}, [loading, shownSession, logout, getToken]);
|
|
348
390
|
if (authState.isLoading) {
|
|
349
391
|
return /* @__PURE__ */ jsx(Fragment, { children: displayWhileLoading ?? null });
|
|
350
392
|
}
|