@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 +37 -0
- package/dist/auth/AthenaSSOProvider.cjs +202 -103
- package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
- package/dist/auth/AthenaSSOProvider.d.ts +35 -4
- package/dist/auth/AthenaSSOProvider.js +202 -103
- package/dist/auth/AthenaSSOProvider.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
- package/dist/diagnostics/athena-diagnostics.d.ts +1 -1
- package/dist/diagnostics/athena-diagnostics.js.map +1 -1
- package/dist/diagnostics/errors.cjs +7 -0
- package/dist/diagnostics/errors.cjs.map +1 -1
- package/dist/diagnostics/errors.d.ts +7 -0
- package/dist/diagnostics/errors.js +7 -0
- package/dist/diagnostics/errors.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
178
|
+
onError,
|
|
179
|
+
revalidateOnWindowFocus = false,
|
|
180
|
+
retryPolicy = SSO_USERINFO_RETRY_POLICY
|
|
58
181
|
}) {
|
|
59
|
-
const [
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
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
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
141
|
-
return read;
|
|
222
|
+
throw readError;
|
|
223
|
+
}
|
|
142
224
|
},
|
|
143
225
|
[ssoUrls.userInfo]
|
|
144
226
|
);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
157
|
-
void
|
|
251
|
+
const revalidate = () => {
|
|
252
|
+
void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });
|
|
158
253
|
};
|
|
159
254
|
const handleVisibilityChange = () => {
|
|
160
255
|
if (document.visibilityState === "visible") {
|
|
161
|
-
|
|
256
|
+
revalidate();
|
|
162
257
|
}
|
|
163
258
|
};
|
|
164
|
-
window.addEventListener("focus",
|
|
259
|
+
window.addEventListener("focus", revalidate);
|
|
165
260
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
166
261
|
return () => {
|
|
167
|
-
window.removeEventListener("focus",
|
|
262
|
+
window.removeEventListener("focus", revalidate);
|
|
168
263
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
169
264
|
};
|
|
170
|
-
}, [revalidateOnWindowFocus,
|
|
265
|
+
}, [revalidateOnWindowFocus, queryClient, queryKey]);
|
|
171
266
|
react.useEffect(() => {
|
|
172
|
-
if (!loading &&
|
|
267
|
+
if (!loading && reason === "unauthenticated" && !signedOut && !displayOnError && typeof window !== "undefined") {
|
|
173
268
|
window.location.assign(ssoUrls.login);
|
|
174
269
|
}
|
|
175
|
-
}, [loading,
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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 =
|
|
311
|
+
return ((_a = sessionRef.current) == null ? void 0 : _a.accessToken) ?? null;
|
|
204
312
|
},
|
|
205
|
-
[
|
|
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 (
|
|
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(
|
|
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,
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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;
|