@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
package/README.md
CHANGED
|
@@ -623,8 +623,11 @@ workbench the layout opens no asset column of its own: assets the agent or the
|
|
|
623
623
|
person opens show as the workbench's **Open assets** tab (closing the tab
|
|
624
624
|
closes the asset panel). Maximize is offered only inside `AthenaLayout`, which
|
|
625
625
|
owns the column's width; collapsing a maximized workspace brings the chat back
|
|
626
|
-
beside the rail.
|
|
627
|
-
|
|
626
|
+
beside the rail. Each session keeps its own open or collapsed workspace while
|
|
627
|
+
the layout stays mounted, so `defaultWorkbenchCollapsed` is how a session the
|
|
628
|
+
person has not opened it in starts. `SessionWorkbench` also works on its own
|
|
629
|
+
anywhere under `<AthenaProvider>`; it then keeps its collapsed state itself,
|
|
630
|
+
per session too.
|
|
628
631
|
|
|
629
632
|
Built-in panes and what each reads:
|
|
630
633
|
|
|
@@ -774,6 +777,25 @@ Both use the same bearer / API key as session creation. The full recipe
|
|
|
774
777
|
template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
|
|
775
778
|
exported if you hold the bridge state yourself.
|
|
776
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
|
+
|
|
795
|
+
## What's new in 0.37.0
|
|
796
|
+
|
|
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
|
+
|
|
777
799
|
## What's new in 0.36.1
|
|
778
800
|
|
|
779
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.
|
|
@@ -1490,6 +1512,27 @@ import { AthenaSSOProvider } from '@athenaintel/react/auth';
|
|
|
1490
1512
|
|
|
1491
1513
|
If you need full control, `ssoUserInfoUrl`, `ssoLoginUrl`, and `ssoLogoutUrl` remain available as explicit overrides.
|
|
1492
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
|
+
|
|
1493
1536
|
## Citation Links
|
|
1494
1537
|
|
|
1495
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.
|
|
@@ -96,8 +96,15 @@ const WorkbenchSplitLayout = ({
|
|
|
96
96
|
)
|
|
97
97
|
});
|
|
98
98
|
const layoutState = react.useMemo(
|
|
99
|
-
() => ({
|
|
100
|
-
|
|
99
|
+
() => ({
|
|
100
|
+
collapsed,
|
|
101
|
+
setCollapsed,
|
|
102
|
+
defaultCollapsed: defaultWorkbenchCollapsed,
|
|
103
|
+
maximized,
|
|
104
|
+
setMaximized,
|
|
105
|
+
hostsAssetPanel: true
|
|
106
|
+
}),
|
|
107
|
+
[collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized]
|
|
101
108
|
);
|
|
102
109
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "flex h-full w-full", children: [
|
|
103
110
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AthenaLayout.cjs","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /** Start with the workbench collapsed to its rail. Defaults to `false`. */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),\n [collapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":["useRef","useCallback","useAssetPanelStore","useEffect","jsx","useState","jsxs","AssetPanel","useMemo","cn","WorkbenchLayoutContext"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AthenaLayout.cjs","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /**\n * Whether a session's workbench starts collapsed to its rail. Each session\n * keeps its own open or collapsed workbench while the layout stays mounted,\n * as it keeps its own tabs, so this is how every session the person has not\n * opened it in shows it — a new one, or one switched to. Defaults to `false`.\n */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({\n collapsed,\n setCollapsed,\n defaultCollapsed: defaultWorkbenchCollapsed,\n maximized,\n setMaximized,\n hostsAssetPanel: true,\n }),\n [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":["useRef","useCallback","useAssetPanelStore","useEffect","jsx","useState","jsxs","AssetPanel","useMemo","cn","WorkbenchLayoutContext"],"mappings":"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"}
|
|
@@ -15,7 +15,12 @@ export interface AthenaLayoutProps {
|
|
|
15
15
|
workbench?: ReactNode;
|
|
16
16
|
/** Default width (%) of the workbench column. Defaults to 40. */
|
|
17
17
|
defaultWorkbenchPercent?: number;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Whether a session's workbench starts collapsed to its rail. Each session
|
|
20
|
+
* keeps its own open or collapsed workbench while the layout stays mounted,
|
|
21
|
+
* as it keeps its own tabs, so this is how every session the person has not
|
|
22
|
+
* opened it in shows it — a new one, or one switched to. Defaults to `false`.
|
|
23
|
+
*/
|
|
19
24
|
defaultWorkbenchCollapsed?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
@@ -94,8 +94,15 @@ const WorkbenchSplitLayout = ({
|
|
|
94
94
|
)
|
|
95
95
|
});
|
|
96
96
|
const layoutState = useMemo(
|
|
97
|
-
() => ({
|
|
98
|
-
|
|
97
|
+
() => ({
|
|
98
|
+
collapsed,
|
|
99
|
+
setCollapsed,
|
|
100
|
+
defaultCollapsed: defaultWorkbenchCollapsed,
|
|
101
|
+
maximized,
|
|
102
|
+
setMaximized,
|
|
103
|
+
hostsAssetPanel: true
|
|
104
|
+
}),
|
|
105
|
+
[collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized]
|
|
99
106
|
);
|
|
100
107
|
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "flex h-full w-full", children: [
|
|
101
108
|
/* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AthenaLayout.js","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /** Start with the workbench collapsed to its rail. Defaults to `false`. */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),\n [collapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AthenaLayout.js","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /**\n * Whether a session's workbench starts collapsed to its rail. Each session\n * keeps its own open or collapsed workbench while the layout stays mounted,\n * as it keeps its own tabs, so this is how every session the person has not\n * opened it in shows it — a new one, or one switched to. Defaults to `false`.\n */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({\n collapsed,\n setCollapsed,\n defaultCollapsed: defaultWorkbenchCollapsed,\n maximized,\n setMaximized,\n hostsAssetPanel: true,\n }),\n [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -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 });
|