@athenaintel/react 0.36.1 → 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 +45 -2
- package/dist/assets/AthenaLayout.cjs +9 -2
- package/dist/assets/AthenaLayout.cjs.map +1 -1
- package/dist/assets/AthenaLayout.d.ts +6 -1
- package/dist/assets/AthenaLayout.js +9 -2
- package/dist/assets/AthenaLayout.js.map +1 -1
- 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/dist/workbench/SessionWorkbench.cjs +13 -3
- package/dist/workbench/SessionWorkbench.cjs.map +1 -1
- package/dist/workbench/SessionWorkbench.d.ts +5 -3
- package/dist/workbench/SessionWorkbench.js +14 -4
- package/dist/workbench/SessionWorkbench.js.map +1 -1
- package/dist/workbench/workbench-layout-context.cjs.map +1 -1
- package/dist/workbench/workbench-layout-context.d.ts +2 -0
- package/dist/workbench/workbench-layout-context.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -1,6 +1,127 @@
|
|
|
1
1
|
import { jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { QueryClient, useQuery } from "@tanstack/react-query";
|
|
2
3
|
import { useState, useRef, useMemo, useCallback, useEffect } from "react";
|
|
4
|
+
import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
|
|
5
|
+
import { AthenaSdkError } from "../diagnostics/errors.js";
|
|
6
|
+
import { DEFAULT_MINT_RETRY_POLICY, getMintRetryDelay, isRetryableMintStatus } from "../runtime/auth.js";
|
|
3
7
|
import { AthenaAuthContext } from "./AthenaAuthContext.js";
|
|
8
|
+
const SSO_USERINFO_RETRY_POLICY = {
|
|
9
|
+
...DEFAULT_MINT_RETRY_POLICY,
|
|
10
|
+
timeoutMs: 1e4
|
|
11
|
+
};
|
|
12
|
+
const abortError = () => new DOMException("The session read was cancelled.", "AbortError");
|
|
13
|
+
async function readUserInfoOnce({
|
|
14
|
+
url,
|
|
15
|
+
signal,
|
|
16
|
+
timeoutMs
|
|
17
|
+
}) {
|
|
18
|
+
if (signal.aborted) throw abortError();
|
|
19
|
+
const request = new AbortController();
|
|
20
|
+
const cancel = () => request.abort();
|
|
21
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
22
|
+
let timedOut = false;
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
timedOut = true;
|
|
25
|
+
request.abort();
|
|
26
|
+
}, timeoutMs);
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(url, { credentials: "include", signal: request.signal });
|
|
29
|
+
if (response.status === 401 || response.status === 403) return { kind: "unauthenticated" };
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
return isRetryableMintStatus(response.status) ? { kind: "transient", status: response.status, reason: "status" } : { kind: "failed", status: response.status };
|
|
32
|
+
}
|
|
33
|
+
let data;
|
|
34
|
+
try {
|
|
35
|
+
data = await response.json();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (signal.aborted) throw abortError();
|
|
38
|
+
return timedOut ? { kind: "transient", status: null, reason: "timeout", cause: error } : { kind: "failed", status: response.status };
|
|
39
|
+
}
|
|
40
|
+
return isValidSSOUserInfo(data) ? { kind: "session", data } : { kind: "invalid_response" };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (signal.aborted) throw abortError();
|
|
43
|
+
return { kind: "transient", status: null, reason: timedOut ? "timeout" : "network", cause: error };
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
signal.removeEventListener("abort", cancel);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function readSession({
|
|
50
|
+
url,
|
|
51
|
+
signal,
|
|
52
|
+
policy
|
|
53
|
+
}) {
|
|
54
|
+
const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
|
|
55
|
+
for (let attempts = 1; ; attempts += 1) {
|
|
56
|
+
const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });
|
|
57
|
+
if (attempt.kind === "session") return { kind: "session", data: attempt.data };
|
|
58
|
+
if (attempt.kind === "unauthenticated") return { kind: "no_session", reason: "unauthenticated" };
|
|
59
|
+
if (attempt.kind === "invalid_response") {
|
|
60
|
+
console.error(
|
|
61
|
+
'[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
|
|
62
|
+
);
|
|
63
|
+
return { kind: "no_session", reason: "invalid_response" };
|
|
64
|
+
}
|
|
65
|
+
if (attempt.kind === "transient" && attempts < maxAttempts) {
|
|
66
|
+
const delayMs = getMintRetryDelay({ retry: attempts, policy });
|
|
67
|
+
athenaDiagnostics.emit("auth.sso.userinfo.retry", {
|
|
68
|
+
level: "warn",
|
|
69
|
+
data: {
|
|
70
|
+
status: attempt.status,
|
|
71
|
+
reason: attempt.reason,
|
|
72
|
+
attempt: attempts,
|
|
73
|
+
max_attempts: maxAttempts,
|
|
74
|
+
delay_ms: delayMs
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
await waitBeforeRetry({ delayMs, signal });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw sessionReadError({ url, attempt, attempts });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function waitBeforeRetry({ delayMs, signal }) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
if (signal.aborted) {
|
|
86
|
+
reject(abortError());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const onAbort = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(abortError());
|
|
92
|
+
};
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
signal.removeEventListener("abort", onAbort);
|
|
95
|
+
resolve();
|
|
96
|
+
}, delayMs);
|
|
97
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function sessionReadError({
|
|
101
|
+
url,
|
|
102
|
+
attempt,
|
|
103
|
+
attempts
|
|
104
|
+
}) {
|
|
105
|
+
if (attempt.kind === "failed") {
|
|
106
|
+
const unreadable = attempt.status >= 200 && attempt.status < 300;
|
|
107
|
+
return new AthenaSdkError({
|
|
108
|
+
code: "auth_sso_failed",
|
|
109
|
+
message: unreadable ? `The SSO session read (${url}) answered HTTP ${attempt.status} without a JSON body.` : `The SSO session read (${url}) answered HTTP ${attempt.status}.`,
|
|
110
|
+
hint: "Check that ssoBaseUrl / ssoUserInfoUrl point at the SSO userinfo endpoint.",
|
|
111
|
+
status: attempt.status,
|
|
112
|
+
context: { attempts, reason: unreadable ? "invalid_body" : "status" }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const answer = attempt.status !== null ? `HTTP ${attempt.status}` : attempt.reason === "timeout" ? "a timeout" : "a network error";
|
|
116
|
+
return new AthenaSdkError({
|
|
117
|
+
code: "auth_sso_failed",
|
|
118
|
+
message: `The SSO session read (${url}) failed ${attempts} time${attempts === 1 ? "" : "s"} (last: ${answer}).`,
|
|
119
|
+
hint: "The sign-in service did not answer; the person may still be signed in. Offer a retry (displayOnError receives retry).",
|
|
120
|
+
...attempt.status !== null && { status: attempt.status },
|
|
121
|
+
...attempt.cause !== void 0 && { cause: attempt.cause },
|
|
122
|
+
context: { attempts, reason: attempt.reason }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
4
125
|
const DEFAULT_SSO_PATHS = {
|
|
5
126
|
userInfo: "/api/sso/userinfo",
|
|
6
127
|
login: "/api/sso/initiate",
|
|
@@ -52,13 +173,15 @@ function AthenaSSOProvider({
|
|
|
52
173
|
ssoLogoutUrl,
|
|
53
174
|
displayWhileLoading,
|
|
54
175
|
displayOnError,
|
|
55
|
-
|
|
176
|
+
onError,
|
|
177
|
+
revalidateOnWindowFocus = false,
|
|
178
|
+
retryPolicy = SSO_USERINFO_RETRY_POLICY
|
|
56
179
|
}) {
|
|
57
|
-
const [
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
180
|
+
const [queryClient] = useState(() => new QueryClient());
|
|
181
|
+
const retryPolicyRef = useRef(retryPolicy);
|
|
182
|
+
retryPolicyRef.current = retryPolicy;
|
|
183
|
+
const onErrorRef = useRef(onError);
|
|
184
|
+
onErrorRef.current = onError;
|
|
62
185
|
const ssoUrls = useMemo(
|
|
63
186
|
() => ({
|
|
64
187
|
userInfo: resolveSSOUrl({
|
|
@@ -79,101 +202,75 @@ function AthenaSSOProvider({
|
|
|
79
202
|
}),
|
|
80
203
|
[ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl]
|
|
81
204
|
);
|
|
82
|
-
const
|
|
83
|
-
|
|
205
|
+
const queryKey = useMemo(() => ["athena-sdk", "sso-session", ssoUrls.userInfo], [ssoUrls.userInfo]);
|
|
206
|
+
const queryFn = useCallback(
|
|
207
|
+
async ({ signal }) => {
|
|
84
208
|
var _a;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const response = await fetch(ssoUrls.userInfo, {
|
|
96
|
-
credentials: "include",
|
|
97
|
-
signal: abortController.signal
|
|
98
|
-
});
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
if (response.status === 401 || response.status === 403) {
|
|
101
|
-
setSSOData(null);
|
|
102
|
-
setError("unauthenticated");
|
|
103
|
-
} else {
|
|
104
|
-
setError("request_failed");
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
const data = await response.json();
|
|
109
|
-
if (!isValidSSOUserInfo(data)) {
|
|
110
|
-
console.error(
|
|
111
|
-
'[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
|
|
112
|
-
);
|
|
113
|
-
setSSOData(null);
|
|
114
|
-
setError("invalid_response");
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
setSSOData((prev) => {
|
|
118
|
-
if (prev && prev.accessToken === data.accessToken && prev.user.user_id === data.user.user_id) {
|
|
119
|
-
return prev;
|
|
120
|
-
}
|
|
121
|
-
return data;
|
|
122
|
-
});
|
|
123
|
-
setError(null);
|
|
124
|
-
return data;
|
|
125
|
-
} catch (fetchError) {
|
|
126
|
-
if (fetchError instanceof DOMException && fetchError.name === "AbortError") {
|
|
127
|
-
return activeRequestRef.current !== abortController ? activeReadRef.current : null;
|
|
128
|
-
}
|
|
129
|
-
setError("request_failed");
|
|
130
|
-
return null;
|
|
131
|
-
} finally {
|
|
132
|
-
if (activeRequestRef.current === abortController) {
|
|
133
|
-
activeRequestRef.current = null;
|
|
134
|
-
setLoading(false);
|
|
209
|
+
try {
|
|
210
|
+
return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });
|
|
211
|
+
} catch (readError) {
|
|
212
|
+
if (readError instanceof AthenaSdkError) {
|
|
213
|
+
athenaDiagnostics.emit("auth.sso.userinfo.error", { error: readError });
|
|
214
|
+
try {
|
|
215
|
+
(_a = onErrorRef.current) == null ? void 0 : _a.call(onErrorRef, readError);
|
|
216
|
+
} catch (callbackError) {
|
|
217
|
+
console.error("[AthenaSDK] AthenaSSOProvider onError threw", callbackError);
|
|
135
218
|
}
|
|
136
219
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return read;
|
|
220
|
+
throw readError;
|
|
221
|
+
}
|
|
140
222
|
},
|
|
141
223
|
[ssoUrls.userInfo]
|
|
142
224
|
);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
225
|
+
const query = useQuery(
|
|
226
|
+
{
|
|
227
|
+
queryKey,
|
|
228
|
+
queryFn,
|
|
229
|
+
// `readSession` retries a transient failure itself, with the bridge's
|
|
230
|
+
// policy; the query never retries on top of it, and reads while the
|
|
231
|
+
// browser reports itself offline too, so that policy decides.
|
|
232
|
+
retry: false,
|
|
233
|
+
networkMode: "always",
|
|
234
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
235
|
+
refetchOnWindowFocus: false,
|
|
236
|
+
refetchOnReconnect: false
|
|
237
|
+
},
|
|
238
|
+
queryClient
|
|
239
|
+
);
|
|
240
|
+
const answer = query.data;
|
|
241
|
+
const session = (answer == null ? void 0 : answer.kind) === "session" ? answer.data : null;
|
|
242
|
+
const loading = query.isPending;
|
|
243
|
+
const reason = session ? null : (answer == null ? void 0 : answer.kind) === "no_session" ? answer.reason : query.isError ? "request_failed" : null;
|
|
244
|
+
const signedOut = (answer == null ? void 0 : answer.kind) === "no_session" && answer.signedOut === true;
|
|
150
245
|
useEffect(() => {
|
|
151
246
|
if (!revalidateOnWindowFocus || typeof window === "undefined") {
|
|
152
247
|
return;
|
|
153
248
|
}
|
|
154
|
-
const
|
|
155
|
-
void
|
|
249
|
+
const revalidate = () => {
|
|
250
|
+
void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });
|
|
156
251
|
};
|
|
157
252
|
const handleVisibilityChange = () => {
|
|
158
253
|
if (document.visibilityState === "visible") {
|
|
159
|
-
|
|
254
|
+
revalidate();
|
|
160
255
|
}
|
|
161
256
|
};
|
|
162
|
-
window.addEventListener("focus",
|
|
257
|
+
window.addEventListener("focus", revalidate);
|
|
163
258
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
164
259
|
return () => {
|
|
165
|
-
window.removeEventListener("focus",
|
|
260
|
+
window.removeEventListener("focus", revalidate);
|
|
166
261
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
167
262
|
};
|
|
168
|
-
}, [revalidateOnWindowFocus,
|
|
263
|
+
}, [revalidateOnWindowFocus, queryClient, queryKey]);
|
|
169
264
|
useEffect(() => {
|
|
170
|
-
if (!loading &&
|
|
265
|
+
if (!loading && reason === "unauthenticated" && !signedOut && !displayOnError && typeof window !== "undefined") {
|
|
171
266
|
window.location.assign(ssoUrls.login);
|
|
172
267
|
}
|
|
173
|
-
}, [loading,
|
|
268
|
+
}, [loading, reason, signedOut, displayOnError, ssoUrls.login]);
|
|
269
|
+
const retry = useCallback(() => {
|
|
270
|
+
void queryClient.resetQueries({ queryKey });
|
|
271
|
+
}, [queryClient, queryKey]);
|
|
174
272
|
const logout = useCallback(async (redirectOnLogout = true) => {
|
|
175
|
-
|
|
176
|
-
(_a = activeRequestRef.current) == null ? void 0 : _a.abort();
|
|
273
|
+
await queryClient.cancelQueries({ queryKey });
|
|
177
274
|
try {
|
|
178
275
|
await fetch(ssoUrls.logout, {
|
|
179
276
|
method: "POST",
|
|
@@ -182,25 +279,36 @@ function AthenaSSOProvider({
|
|
|
182
279
|
} catch (logoutError) {
|
|
183
280
|
console.error("[AthenaSDK] Failed to invalidate SSO session during logout", logoutError);
|
|
184
281
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
282
|
+
queryClient.setQueryData(queryKey, {
|
|
283
|
+
kind: "no_session",
|
|
284
|
+
reason: "unauthenticated",
|
|
285
|
+
signedOut: true
|
|
286
|
+
});
|
|
188
287
|
if (redirectOnLogout && typeof window !== "undefined") {
|
|
189
288
|
window.location.assign(ssoUrls.login);
|
|
190
289
|
}
|
|
191
|
-
}, [ssoUrls.login, ssoUrls.logout]);
|
|
192
|
-
const
|
|
193
|
-
|
|
290
|
+
}, [queryClient, queryKey, ssoUrls.login, ssoUrls.logout]);
|
|
291
|
+
const sessionRef = useRef(session);
|
|
292
|
+
sessionRef.current = session;
|
|
194
293
|
const getToken = useCallback(
|
|
195
294
|
async ({ force = false } = {}) => {
|
|
196
295
|
var _a;
|
|
197
296
|
if (force) {
|
|
198
|
-
|
|
199
|
-
|
|
297
|
+
try {
|
|
298
|
+
const renewed = await queryClient.fetchQuery({
|
|
299
|
+
queryKey,
|
|
300
|
+
queryFn,
|
|
301
|
+
staleTime: 0,
|
|
302
|
+
retry: false,
|
|
303
|
+
networkMode: "always"
|
|
304
|
+
});
|
|
305
|
+
if (renewed.kind === "session") return renewed.data.accessToken;
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
200
308
|
}
|
|
201
|
-
return ((_a =
|
|
309
|
+
return ((_a = sessionRef.current) == null ? void 0 : _a.accessToken) ?? null;
|
|
202
310
|
},
|
|
203
|
-
[
|
|
311
|
+
[queryClient, queryKey, queryFn]
|
|
204
312
|
);
|
|
205
313
|
const authState = useMemo(() => {
|
|
206
314
|
if (loading) {
|
|
@@ -215,19 +323,7 @@ function AthenaSSOProvider({
|
|
|
215
323
|
getToken
|
|
216
324
|
};
|
|
217
325
|
}
|
|
218
|
-
if (
|
|
219
|
-
const { user: user2, orgs: orgs2, accessToken: accessToken2 } = mapSSOResponse(ssoData);
|
|
220
|
-
return {
|
|
221
|
-
isLoggedIn: true,
|
|
222
|
-
isLoading: false,
|
|
223
|
-
user: user2,
|
|
224
|
-
accessToken: accessToken2,
|
|
225
|
-
orgs: orgs2,
|
|
226
|
-
logout,
|
|
227
|
-
getToken
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
if (error || !ssoData) {
|
|
326
|
+
if (!session) {
|
|
231
327
|
return {
|
|
232
328
|
isLoggedIn: false,
|
|
233
329
|
isLoading: false,
|
|
@@ -238,7 +334,7 @@ function AthenaSSOProvider({
|
|
|
238
334
|
getToken
|
|
239
335
|
};
|
|
240
336
|
}
|
|
241
|
-
const { user, orgs, accessToken } = mapSSOResponse(
|
|
337
|
+
const { user, orgs, accessToken } = mapSSOResponse(session);
|
|
242
338
|
return {
|
|
243
339
|
isLoggedIn: true,
|
|
244
340
|
isLoading: false,
|
|
@@ -248,11 +344,14 @@ function AthenaSSOProvider({
|
|
|
248
344
|
logout,
|
|
249
345
|
getToken
|
|
250
346
|
};
|
|
251
|
-
}, [loading,
|
|
347
|
+
}, [loading, session, logout, getToken]);
|
|
252
348
|
if (authState.isLoading) {
|
|
253
349
|
return /* @__PURE__ */ jsx(Fragment, { children: displayWhileLoading ?? null });
|
|
254
350
|
}
|
|
255
351
|
if (!authState.isLoggedIn) {
|
|
352
|
+
if (typeof displayOnError === "function") {
|
|
353
|
+
return /* @__PURE__ */ jsx(Fragment, { children: displayOnError({ reason: reason ?? "unauthenticated", retry }) });
|
|
354
|
+
}
|
|
256
355
|
return displayOnError ? /* @__PURE__ */ jsx(Fragment, { children: displayOnError }) : null;
|
|
257
356
|
}
|
|
258
357
|
return /* @__PURE__ */ jsx(AthenaAuthContext.Provider, { value: authState, children });
|