@athenaintel/react 0.37.0 → 0.38.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 CHANGED
@@ -777,9 +777,25 @@ Both use the same bearer / API key as session creation. The full recipe
777
777
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
778
778
  exported if you hold the bridge state yourself.
779
779
 
780
+ ## What's new in 0.38.0
781
+
782
+ **A brief sign-in hiccup no longer shows your app's sign-in screen.** `AthenaSSOProvider` read the session (`/api/sso/userinfo`) once and showed `displayOnError` on any failure, so a person who was signed in saw the sign-in screen until they reloaded. On computer apps this happens when several tabs load at once: Marathon's credential mint is briefly rate limited and answers `502 {"error":"mint_failed"}`. Each read is now one bounded cycle, the one the parent bridge runs for `/_athena/auth`:
783
+
784
+ - A transient failure (5xx, 408, 429, a network error, or no answer within 10 s) is retried with capped exponential backoff and jitter: at most 4 requests in all, about 0.5 s, 1 s and 2 s apart.
785
+ - A 401 or 403 (no session) is not retried, and neither is another status or a 2xx without a JSON body.
786
+ - A read that gives up reports one `AthenaSdkError`, with the new code `auth_sso_failed`, the HTTP `status` and `context.attempts`, to the provider's new `onError` prop and on the diagnostics bus. Pass `onError` to `AthenaSSOProvider` to observe it: an `AthenaProvider` nested inside is mounted only after sign-in, so its `onError` cannot see the first read. Nothing reads again on its own after that, except a forced token renewal or a focus revalidation (`revalidateOnWindowFocus`), each one bounded read.
787
+ - A focus revalidation or a forced renewal that starts while a read is in flight joins that read instead of cancelling it, so one mint answers both. A read that fails keeps the session an earlier read answered.
788
+
789
+ **`displayOnError` can tell a signed-out person from a failed read.** Pass a function and it receives `{ reason, retry }`. `reason` is `unauthenticated` (no session: offer sign-in), `request_failed` (the endpoint did not answer usably, even after retrying: the person may well be signed in, so offer `retry`) or `invalid_response`. `retry()` reads the session again and shows `displayWhileLoading` meanwhile. An element works as before. The types are `AthenaSSOFailure` and `AthenaSSOFailureReason`, from `@athenaintel/react/auth`.
790
+
791
+ New diagnostics event: `auth.sso.userinfo.retry` (level `warn`, one for each retry, with `status`, `reason`, `attempt`, `max_attempts` and `delay_ms`). A read that gives up emits `auth.sso.userinfo.error`.
792
+
793
+ No host code changes are needed. To stop showing a sign-in screen after a failed read, pass `displayOnError` as a function and branch on `reason` (see [Loading, sign-in and failures](#loading-sign-in-and-failures)).
794
+
780
795
  ## What's new in 0.37.0
781
796
 
782
797
  **Each session keeps its own open or collapsed workspace.** `SessionWorkbench` already kept each session's tabs; it now keeps whether its workspace is open too. A session the person has not opened or collapsed it in, a new one or one switched to, shows `AthenaLayout`'s `defaultWorkbenchCollapsed`. With `defaultWorkbenchCollapsed`, every session starts with the workspace on its rail, and opening it in one session no longer opens it in the next. An asset the agent or the person opens still brings it out, for that session. Without the prop nothing changes for a session that has not collapsed it, but collapsing one no longer collapses the others.
798
+
783
799
  ## What's new in 0.36.1
784
800
 
785
801
  **Opening a long session no longer jumps the transcript around.** As older history pages in above what you are reading, the reading position now stays put, instantly. Before, the chat added its own scroll correction on top of the browser's scroll anchoring (a jump by a page's height), let it play through the viewport's smooth scrolling (a fast scroll through the transcript), and missed a page that folds into the first loaded turn, which moved the text under the reader. Cut tool outputs are now read only for messages near the viewport, at most two at a time, instead of every loaded step at once: opening a long agent run sent dozens of reads, each re-rendering the transcript and all queued ahead of your next message. Nothing to change in your app.
@@ -1496,6 +1512,27 @@ import { AthenaSSOProvider } from '@athenaintel/react/auth';
1496
1512
 
1497
1513
  If you need full control, `ssoUserInfoUrl`, `ssoLoginUrl`, and `ssoLogoutUrl` remain available as explicit overrides.
1498
1514
 
1515
+ ### Loading, sign-in and failures
1516
+
1517
+ ```tsx
1518
+ <AthenaSSOProvider
1519
+ displayWhileLoading={<AppSkeleton />}
1520
+ displayOnError={({ reason, retry }) =>
1521
+ reason === 'unauthenticated' ? <SignIn /> : <SignInCheckFailed onRetry={retry} />
1522
+ }
1523
+ >
1524
+ <AthenaProvider>
1525
+ <AthenaChat />
1526
+ </AthenaProvider>
1527
+ </AthenaSSOProvider>
1528
+ ```
1529
+
1530
+ - `displayWhileLoading` shows while the provider reads the session. On a normal load that takes well under a second, so an outline of your app reads better than words.
1531
+ - A transient failure (5xx, 408, 429, a network error, a timeout) is retried within the read, about 0.5 s, 1 s and 2 s apart, before `displayOnError` shows.
1532
+ - Only `unauthenticated` means the person has no session. `request_failed` means the endpoint did not answer usably; the person may well be signed in, so offer `retry` rather than sign-in.
1533
+ - `onError` receives the typed `auth_sso_failed` error of a read that gave up.
1534
+ - Without `displayOnError`, a person with no session is sent to `ssoLoginUrl`.
1535
+
1499
1536
  ## Citation Links
1500
1537
 
1501
1538
  When you render chat inside `<AthenaLayout>`, Athena citation links (`app.athenaintel.com/dashboard/spaces?...`) now open the referenced asset in the SDK asset pane by default instead of navigating away.
@@ -1,8 +1,129 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
+ const reactQuery = require("@tanstack/react-query");
4
5
  const react = require("react");
6
+ const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
7
+ const errors = require("../diagnostics/errors.cjs");
8
+ const auth = require("../runtime/auth.cjs");
5
9
  const AthenaAuthContext = require("./AthenaAuthContext.cjs");
10
+ const SSO_USERINFO_RETRY_POLICY = {
11
+ ...auth.DEFAULT_MINT_RETRY_POLICY,
12
+ timeoutMs: 1e4
13
+ };
14
+ const abortError = () => new DOMException("The session read was cancelled.", "AbortError");
15
+ async function readUserInfoOnce({
16
+ url,
17
+ signal,
18
+ timeoutMs
19
+ }) {
20
+ if (signal.aborted) throw abortError();
21
+ const request = new AbortController();
22
+ const cancel = () => request.abort();
23
+ signal.addEventListener("abort", cancel, { once: true });
24
+ let timedOut = false;
25
+ const timer = setTimeout(() => {
26
+ timedOut = true;
27
+ request.abort();
28
+ }, timeoutMs);
29
+ try {
30
+ const response = await fetch(url, { credentials: "include", signal: request.signal });
31
+ if (response.status === 401 || response.status === 403) return { kind: "unauthenticated" };
32
+ if (!response.ok) {
33
+ return auth.isRetryableMintStatus(response.status) ? { kind: "transient", status: response.status, reason: "status" } : { kind: "failed", status: response.status };
34
+ }
35
+ let data;
36
+ try {
37
+ data = await response.json();
38
+ } catch (error) {
39
+ if (signal.aborted) throw abortError();
40
+ return timedOut ? { kind: "transient", status: null, reason: "timeout", cause: error } : { kind: "failed", status: response.status };
41
+ }
42
+ return isValidSSOUserInfo(data) ? { kind: "session", data } : { kind: "invalid_response" };
43
+ } catch (error) {
44
+ if (signal.aborted) throw abortError();
45
+ return { kind: "transient", status: null, reason: timedOut ? "timeout" : "network", cause: error };
46
+ } finally {
47
+ clearTimeout(timer);
48
+ signal.removeEventListener("abort", cancel);
49
+ }
50
+ }
51
+ async function readSession({
52
+ url,
53
+ signal,
54
+ policy
55
+ }) {
56
+ const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
57
+ for (let attempts = 1; ; attempts += 1) {
58
+ const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });
59
+ if (attempt.kind === "session") return { kind: "session", data: attempt.data };
60
+ if (attempt.kind === "unauthenticated") return { kind: "no_session", reason: "unauthenticated" };
61
+ if (attempt.kind === "invalid_response") {
62
+ console.error(
63
+ '[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
64
+ );
65
+ return { kind: "no_session", reason: "invalid_response" };
66
+ }
67
+ if (attempt.kind === "transient" && attempts < maxAttempts) {
68
+ const delayMs = auth.getMintRetryDelay({ retry: attempts, policy });
69
+ athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.retry", {
70
+ level: "warn",
71
+ data: {
72
+ status: attempt.status,
73
+ reason: attempt.reason,
74
+ attempt: attempts,
75
+ max_attempts: maxAttempts,
76
+ delay_ms: delayMs
77
+ }
78
+ });
79
+ await waitBeforeRetry({ delayMs, signal });
80
+ continue;
81
+ }
82
+ throw sessionReadError({ url, attempt, attempts });
83
+ }
84
+ }
85
+ function waitBeforeRetry({ delayMs, signal }) {
86
+ return new Promise((resolve, reject) => {
87
+ if (signal.aborted) {
88
+ reject(abortError());
89
+ return;
90
+ }
91
+ const onAbort = () => {
92
+ clearTimeout(timer);
93
+ reject(abortError());
94
+ };
95
+ const timer = setTimeout(() => {
96
+ signal.removeEventListener("abort", onAbort);
97
+ resolve();
98
+ }, delayMs);
99
+ signal.addEventListener("abort", onAbort, { once: true });
100
+ });
101
+ }
102
+ function sessionReadError({
103
+ url,
104
+ attempt,
105
+ attempts
106
+ }) {
107
+ if (attempt.kind === "failed") {
108
+ const unreadable = attempt.status >= 200 && attempt.status < 300;
109
+ return new errors.AthenaSdkError({
110
+ code: "auth_sso_failed",
111
+ message: unreadable ? `The SSO session read (${url}) answered HTTP ${attempt.status} without a JSON body.` : `The SSO session read (${url}) answered HTTP ${attempt.status}.`,
112
+ hint: "Check that ssoBaseUrl / ssoUserInfoUrl point at the SSO userinfo endpoint.",
113
+ status: attempt.status,
114
+ context: { attempts, reason: unreadable ? "invalid_body" : "status" }
115
+ });
116
+ }
117
+ const answer = attempt.status !== null ? `HTTP ${attempt.status}` : attempt.reason === "timeout" ? "a timeout" : "a network error";
118
+ return new errors.AthenaSdkError({
119
+ code: "auth_sso_failed",
120
+ message: `The SSO session read (${url}) failed ${attempts} time${attempts === 1 ? "" : "s"} (last: ${answer}).`,
121
+ hint: "The sign-in service did not answer; the person may still be signed in. Offer a retry (displayOnError receives retry).",
122
+ ...attempt.status !== null && { status: attempt.status },
123
+ ...attempt.cause !== void 0 && { cause: attempt.cause },
124
+ context: { attempts, reason: attempt.reason }
125
+ });
126
+ }
6
127
  const DEFAULT_SSO_PATHS = {
7
128
  userInfo: "/api/sso/userinfo",
8
129
  login: "/api/sso/initiate",
@@ -54,13 +175,15 @@ function AthenaSSOProvider({
54
175
  ssoLogoutUrl,
55
176
  displayWhileLoading,
56
177
  displayOnError,
57
- revalidateOnWindowFocus = false
178
+ onError,
179
+ revalidateOnWindowFocus = false,
180
+ retryPolicy = SSO_USERINFO_RETRY_POLICY
58
181
  }) {
59
- const [ssoData, setSSOData] = react.useState(null);
60
- const [loading, setLoading] = react.useState(true);
61
- const [error, setError] = react.useState(null);
62
- const activeRequestRef = react.useRef(null);
63
- const activeReadRef = react.useRef(null);
182
+ const [queryClient] = react.useState(() => new reactQuery.QueryClient());
183
+ const retryPolicyRef = react.useRef(retryPolicy);
184
+ retryPolicyRef.current = retryPolicy;
185
+ const onErrorRef = react.useRef(onError);
186
+ onErrorRef.current = onError;
64
187
  const ssoUrls = react.useMemo(
65
188
  () => ({
66
189
  userInfo: resolveSSOUrl({
@@ -81,101 +204,75 @@ function AthenaSSOProvider({
81
204
  }),
82
205
  [ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl]
83
206
  );
84
- const refreshSession = react.useCallback(
85
- ({ showLoading = false } = {}) => {
207
+ const queryKey = react.useMemo(() => ["athena-sdk", "sso-session", ssoUrls.userInfo], [ssoUrls.userInfo]);
208
+ const queryFn = react.useCallback(
209
+ async ({ signal }) => {
86
210
  var _a;
87
- (_a = activeRequestRef.current) == null ? void 0 : _a.abort();
88
- const abortController = new AbortController();
89
- activeRequestRef.current = abortController;
90
- const read = (async () => {
91
- if (showLoading) {
92
- setLoading(true);
93
- setError(null);
94
- setSSOData(null);
95
- }
96
- try {
97
- const response = await fetch(ssoUrls.userInfo, {
98
- credentials: "include",
99
- signal: abortController.signal
100
- });
101
- if (!response.ok) {
102
- if (response.status === 401 || response.status === 403) {
103
- setSSOData(null);
104
- setError("unauthenticated");
105
- } else {
106
- setError("request_failed");
107
- }
108
- return null;
109
- }
110
- const data = await response.json();
111
- if (!isValidSSOUserInfo(data)) {
112
- console.error(
113
- '[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
114
- );
115
- setSSOData(null);
116
- setError("invalid_response");
117
- return null;
118
- }
119
- setSSOData((prev) => {
120
- if (prev && prev.accessToken === data.accessToken && prev.user.user_id === data.user.user_id) {
121
- return prev;
122
- }
123
- return data;
124
- });
125
- setError(null);
126
- return data;
127
- } catch (fetchError) {
128
- if (fetchError instanceof DOMException && fetchError.name === "AbortError") {
129
- return activeRequestRef.current !== abortController ? activeReadRef.current : null;
130
- }
131
- setError("request_failed");
132
- return null;
133
- } finally {
134
- if (activeRequestRef.current === abortController) {
135
- activeRequestRef.current = null;
136
- setLoading(false);
211
+ try {
212
+ return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });
213
+ } catch (readError) {
214
+ if (readError instanceof errors.AthenaSdkError) {
215
+ athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.error", { error: readError });
216
+ try {
217
+ (_a = onErrorRef.current) == null ? void 0 : _a.call(onErrorRef, readError);
218
+ } catch (callbackError) {
219
+ console.error("[AthenaSDK] AthenaSSOProvider onError threw", callbackError);
137
220
  }
138
221
  }
139
- })();
140
- activeReadRef.current = read;
141
- return read;
222
+ throw readError;
223
+ }
142
224
  },
143
225
  [ssoUrls.userInfo]
144
226
  );
145
- react.useEffect(() => {
146
- void refreshSession({ showLoading: true });
147
- return () => {
148
- var _a;
149
- (_a = activeRequestRef.current) == null ? void 0 : _a.abort();
150
- };
151
- }, [refreshSession]);
227
+ const query = reactQuery.useQuery(
228
+ {
229
+ queryKey,
230
+ queryFn,
231
+ // `readSession` retries a transient failure itself, with the bridge's
232
+ // policy; the query never retries on top of it, and reads while the
233
+ // browser reports itself offline too, so that policy decides.
234
+ retry: false,
235
+ networkMode: "always",
236
+ staleTime: Number.POSITIVE_INFINITY,
237
+ refetchOnWindowFocus: false,
238
+ refetchOnReconnect: false
239
+ },
240
+ queryClient
241
+ );
242
+ const answer = query.data;
243
+ const session = (answer == null ? void 0 : answer.kind) === "session" ? answer.data : null;
244
+ const loading = query.isPending;
245
+ const reason = session ? null : (answer == null ? void 0 : answer.kind) === "no_session" ? answer.reason : query.isError ? "request_failed" : null;
246
+ const signedOut = (answer == null ? void 0 : answer.kind) === "no_session" && answer.signedOut === true;
152
247
  react.useEffect(() => {
153
248
  if (!revalidateOnWindowFocus || typeof window === "undefined") {
154
249
  return;
155
250
  }
156
- const handleFocus = () => {
157
- void refreshSession();
251
+ const revalidate = () => {
252
+ void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });
158
253
  };
159
254
  const handleVisibilityChange = () => {
160
255
  if (document.visibilityState === "visible") {
161
- void refreshSession();
256
+ revalidate();
162
257
  }
163
258
  };
164
- window.addEventListener("focus", handleFocus);
259
+ window.addEventListener("focus", revalidate);
165
260
  document.addEventListener("visibilitychange", handleVisibilityChange);
166
261
  return () => {
167
- window.removeEventListener("focus", handleFocus);
262
+ window.removeEventListener("focus", revalidate);
168
263
  document.removeEventListener("visibilitychange", handleVisibilityChange);
169
264
  };
170
- }, [revalidateOnWindowFocus, refreshSession]);
265
+ }, [revalidateOnWindowFocus, queryClient, queryKey]);
171
266
  react.useEffect(() => {
172
- if (!loading && error === "unauthenticated" && !displayOnError && typeof window !== "undefined") {
267
+ if (!loading && reason === "unauthenticated" && !signedOut && !displayOnError && typeof window !== "undefined") {
173
268
  window.location.assign(ssoUrls.login);
174
269
  }
175
- }, [loading, error, displayOnError, ssoUrls.login]);
270
+ }, [loading, reason, signedOut, displayOnError, ssoUrls.login]);
271
+ const retry = react.useCallback(() => {
272
+ void queryClient.resetQueries({ queryKey });
273
+ }, [queryClient, queryKey]);
176
274
  const logout = react.useCallback(async (redirectOnLogout = true) => {
177
- var _a;
178
- (_a = activeRequestRef.current) == null ? void 0 : _a.abort();
275
+ await queryClient.cancelQueries({ queryKey });
179
276
  try {
180
277
  await fetch(ssoUrls.logout, {
181
278
  method: "POST",
@@ -184,25 +281,36 @@ function AthenaSSOProvider({
184
281
  } catch (logoutError) {
185
282
  console.error("[AthenaSDK] Failed to invalidate SSO session during logout", logoutError);
186
283
  }
187
- setSSOData(null);
188
- setError(null);
189
- setLoading(false);
284
+ queryClient.setQueryData(queryKey, {
285
+ kind: "no_session",
286
+ reason: "unauthenticated",
287
+ signedOut: true
288
+ });
190
289
  if (redirectOnLogout && typeof window !== "undefined") {
191
290
  window.location.assign(ssoUrls.login);
192
291
  }
193
- }, [ssoUrls.login, ssoUrls.logout]);
194
- const ssoDataRef = react.useRef(ssoData);
195
- ssoDataRef.current = ssoData;
292
+ }, [queryClient, queryKey, ssoUrls.login, ssoUrls.logout]);
293
+ const sessionRef = react.useRef(session);
294
+ sessionRef.current = session;
196
295
  const getToken = react.useCallback(
197
296
  async ({ force = false } = {}) => {
198
297
  var _a;
199
298
  if (force) {
200
- const renewed = await refreshSession();
201
- if (renewed) return renewed.accessToken;
299
+ try {
300
+ const renewed = await queryClient.fetchQuery({
301
+ queryKey,
302
+ queryFn,
303
+ staleTime: 0,
304
+ retry: false,
305
+ networkMode: "always"
306
+ });
307
+ if (renewed.kind === "session") return renewed.data.accessToken;
308
+ } catch {
309
+ }
202
310
  }
203
- return ((_a = ssoDataRef.current) == null ? void 0 : _a.accessToken) ?? null;
311
+ return ((_a = sessionRef.current) == null ? void 0 : _a.accessToken) ?? null;
204
312
  },
205
- [refreshSession]
313
+ [queryClient, queryKey, queryFn]
206
314
  );
207
315
  const authState = react.useMemo(() => {
208
316
  if (loading) {
@@ -217,19 +325,7 @@ function AthenaSSOProvider({
217
325
  getToken
218
326
  };
219
327
  }
220
- if (error === "request_failed" && ssoData) {
221
- const { user: user2, orgs: orgs2, accessToken: accessToken2 } = mapSSOResponse(ssoData);
222
- return {
223
- isLoggedIn: true,
224
- isLoading: false,
225
- user: user2,
226
- accessToken: accessToken2,
227
- orgs: orgs2,
228
- logout,
229
- getToken
230
- };
231
- }
232
- if (error || !ssoData) {
328
+ if (!session) {
233
329
  return {
234
330
  isLoggedIn: false,
235
331
  isLoading: false,
@@ -240,7 +336,7 @@ function AthenaSSOProvider({
240
336
  getToken
241
337
  };
242
338
  }
243
- const { user, orgs, accessToken } = mapSSOResponse(ssoData);
339
+ const { user, orgs, accessToken } = mapSSOResponse(session);
244
340
  return {
245
341
  isLoggedIn: true,
246
342
  isLoading: false,
@@ -250,11 +346,14 @@ function AthenaSSOProvider({
250
346
  logout,
251
347
  getToken
252
348
  };
253
- }, [loading, error, ssoData, logout, getToken]);
349
+ }, [loading, session, logout, getToken]);
254
350
  if (authState.isLoading) {
255
351
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayWhileLoading ?? null });
256
352
  }
257
353
  if (!authState.isLoggedIn) {
354
+ if (typeof displayOnError === "function") {
355
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayOnError({ reason: reason ?? "unauthenticated", retry }) });
356
+ }
258
357
  return displayOnError ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayOnError }) : null;
259
358
  }
260
359
  return /* @__PURE__ */ jsxRuntime.jsx(AthenaAuthContext.AthenaAuthContext.Provider, { value: authState, children });
@@ -1 +1 @@
1
- {"version":3,"file":"AthenaSSOProvider.cjs","sources":["../../src/auth/AthenaSSOProvider.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\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 * Custom element displayed when SSO authentication fails.\n * If not provided, the user is automatically redirected to `ssoLoginUrl`.\n */\n displayOnError?: ReactNode;\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\n// ─── Helpers ──────────────────────────────────────────────────────────\n\ntype AthenaSSOError = 'unauthenticated' | 'invalid_response' | 'request_failed';\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 revalidateOnWindowFocus = false,\n}: AthenaSSOProviderProps) {\n const [ssoData, setSSOData] = useState<AthenaSSOUserInfo | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<AthenaSSOError | null>(null);\n const activeRequestRef = useRef<AbortController | null>(null);\n const activeReadRef = useRef<Promise<AthenaSSOUserInfo | null> | null>(null);\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 // Resolves the session it read, or `null` when it read none (refused,\n // failed, or cancelled by logout or unmount), so a token renewal can hand the\n // new token back. A read superseded by a newer one (a focus revalidation\n // that starts during a renewal) resolves with the newer read's session, so\n // background revalidation never cancels the renewal a connection waits on.\n const refreshSession = useCallback(\n ({ showLoading = false }: { showLoading?: boolean } = {}): Promise<AthenaSSOUserInfo | null> => {\n activeRequestRef.current?.abort();\n const abortController = new AbortController();\n activeRequestRef.current = abortController;\n\n const read = (async (): Promise<AthenaSSOUserInfo | null> => {\n if (showLoading) {\n setLoading(true);\n setError(null);\n setSSOData(null);\n }\n\n try {\n const response = await fetch(ssoUrls.userInfo, {\n credentials: 'include',\n signal: abortController.signal,\n });\n\n if (!response.ok) {\n if (response.status === 401 || response.status === 403) {\n setSSOData(null);\n setError('unauthenticated');\n } else {\n setError('request_failed');\n }\n return null;\n }\n\n const data: unknown = await response.json();\n\n if (!isValidSSOUserInfo(data)) {\n console.error(\n '[AthenaSDK] Invalid SSO userinfo response: expected \"user.user_id\", \"user.email\", \"orgMemberInfos\", and \"accessToken\"',\n );\n setSSOData(null);\n setError('invalid_response');\n return null;\n }\n\n setSSOData((prev) => {\n if (\n prev &&\n prev.accessToken === data.accessToken &&\n prev.user.user_id === data.user.user_id\n ) {\n return prev;\n }\n return data;\n });\n setError(null);\n return data;\n } catch (fetchError) {\n if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {\n // Superseded by a newer read: its session is this read's answer.\n // Cancelled by logout or unmount (nothing newer started): none.\n return activeRequestRef.current !== abortController ? activeReadRef.current : null;\n }\n\n setError('request_failed');\n return null;\n } finally {\n if (activeRequestRef.current === abortController) {\n activeRequestRef.current = null;\n setLoading(false);\n }\n }\n })();\n activeReadRef.current = read;\n return read;\n },\n [ssoUrls.userInfo],\n );\n\n useEffect(() => {\n void refreshSession({ showLoading: true });\n\n return () => {\n activeRequestRef.current?.abort();\n };\n }, [refreshSession]);\n\n useEffect(() => {\n if (!revalidateOnWindowFocus || typeof window === 'undefined') {\n return;\n }\n\n const handleFocus = () => {\n void refreshSession();\n };\n const handleVisibilityChange = () => {\n if (document.visibilityState === 'visible') {\n void refreshSession();\n }\n };\n\n window.addEventListener('focus', handleFocus);\n document.addEventListener('visibilitychange', handleVisibilityChange);\n\n return () => {\n window.removeEventListener('focus', handleFocus);\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n };\n }, [revalidateOnWindowFocus, refreshSession]);\n\n // Redirect to SSO login when session is invalid and no error UI provided.\n useEffect(() => {\n if (\n !loading &&\n error === 'unauthenticated' &&\n !displayOnError &&\n typeof window !== 'undefined'\n ) {\n window.location.assign(ssoUrls.login);\n }\n }, [loading, error, displayOnError, ssoUrls.login]);\n\n const logout = useCallback(async (redirectOnLogout = true) => {\n activeRequestRef.current?.abort();\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 setSSOData(null);\n setError(null);\n setLoading(false);\n\n if (redirectOnLogout && typeof window !== 'undefined') {\n window.location.assign(ssoUrls.login);\n }\n }, [ssoUrls.login, ssoUrls.logout]);\n\n // The SSO side of the SDK's credential source: `force` (the previous token\n // was refused) re-reads the session for a new token; otherwise the held\n // one is served.\n const ssoDataRef = useRef(ssoData);\n ssoDataRef.current = ssoData;\n const getToken = useCallback<AthenaStatewireTokenGetter>(\n async ({ force = false } = {}) => {\n if (force) {\n const renewed = await refreshSession();\n if (renewed) return renewed.accessToken;\n }\n return ssoDataRef.current?.accessToken ?? null;\n },\n [refreshSession],\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 (error === 'request_failed' && ssoData) {\n const { user, orgs, accessToken } = mapSSOResponse(ssoData);\n\n return {\n isLoggedIn: true,\n isLoading: false,\n user,\n accessToken,\n orgs,\n logout,\n getToken,\n };\n }\n\n if (error || !ssoData) {\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(ssoData);\n\n return {\n isLoggedIn: true,\n isLoading: false,\n user,\n accessToken,\n orgs,\n logout,\n getToken,\n };\n }, [loading, error, ssoData, logout, getToken]);\n\n if (authState.isLoading) {\n return <>{displayWhileLoading ?? null}</>;\n }\n\n if (!authState.isLoggedIn) {\n return displayOnError ? <>{displayOnError}</> : null;\n }\n\n return (\n <AthenaAuthContext.Provider value={authState}>\n {children}\n </AthenaAuthContext.Provider>\n );\n}\n"],"names":["useState","useRef","useMemo","useCallback","useEffect","user","orgs","accessToken","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 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,4 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
+ import { AthenaSdkError } from '../diagnostics/errors';
3
+ import { type MintRetryPolicy } from '../runtime/auth';
2
4
  export interface AthenaSSOProviderProps {
3
5
  children: ReactNode;
4
6
  /**
@@ -46,16 +48,45 @@ export interface AthenaSSOProviderProps {
46
48
  */
47
49
  displayWhileLoading?: ReactNode;
48
50
  /**
49
- * Custom element displayed when SSO authentication fails.
50
- * If not provided, the user is automatically redirected to `ssoLoginUrl`.
51
+ * Shown when there is no session to sign the app in with: an element, or a
52
+ * function that receives why (`reason`) and a `retry`, so the app can offer
53
+ * sign-in only to a person who is signed out and a retry when the endpoint
54
+ * could not answer (see `AthenaSSOFailure`).
55
+ * If not provided, the user is automatically redirected to `ssoLoginUrl`
56
+ * when the endpoint answers that there is no session.
51
57
  */
52
- displayOnError?: ReactNode;
58
+ displayOnError?: ReactNode | ((failure: AthenaSSOFailure) => ReactNode);
59
+ /**
60
+ * Called with the typed error (`auth_sso_failed`) when a session read gives
61
+ * up. A nested `AthenaProvider`'s `onError` is not mounted while the first
62
+ * read runs, so observe sign-in failures here.
63
+ */
64
+ onError?: (error: AthenaSdkError) => void;
53
65
  /**
54
66
  * Re-validate the SSO session when the window regains focus.
55
67
  * Disabled by default. When enabled, token expiry and user switching
56
68
  * are picked up without requiring a full page reload.
57
69
  */
58
70
  revalidateOnWindowFocus?: boolean;
71
+ /** @internal Overrides how a session read retries; used by tests. */
72
+ retryPolicy?: MintRetryPolicy;
73
+ }
74
+ /**
75
+ * Why the provider has no session.
76
+ *
77
+ * - `unauthenticated`: the endpoint answered 401 or 403, or the person signed
78
+ * out. Offer sign-in.
79
+ * - `request_failed`: the endpoint did not answer usably. A network error, a
80
+ * timeout, or a 5xx / 408 / 429 on every attempt of one read, or another
81
+ * status. The person may well be signed in. Offer `retry`.
82
+ * - `invalid_response`: it answered without a usable session.
83
+ */
84
+ export type AthenaSSOFailureReason = 'unauthenticated' | 'invalid_response' | 'request_failed';
85
+ /** What `displayOnError` receives when it is a function. */
86
+ export interface AthenaSSOFailure {
87
+ reason: AthenaSSOFailureReason;
88
+ /** Read the session again. `displayWhileLoading` shows meanwhile. */
89
+ retry: () => void;
59
90
  }
60
91
  /**
61
92
  * Wraps your app with Athena authentication powered by an external SSO
@@ -88,4 +119,4 @@ export interface AthenaSSOProviderProps {
88
119
  * }
89
120
  * ```
90
121
  */
91
- export declare function AthenaSSOProvider({ children, ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl, displayWhileLoading, displayOnError, revalidateOnWindowFocus, }: AthenaSSOProviderProps): import("react").JSX.Element | null;
122
+ export declare function AthenaSSOProvider({ children, ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl, displayWhileLoading, displayOnError, onError, revalidateOnWindowFocus, retryPolicy, }: AthenaSSOProviderProps): import("react").JSX.Element | null;