@data-club/ai-hub 0.0.1
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/dist/auth/passwordPolicy.d.ts +55 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +63 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/texts.d.ts +815 -0
- package/dist/auth/texts.d.ts.map +1 -0
- package/dist/auth/texts.js +429 -0
- package/dist/auth/texts.js.map +1 -0
- package/dist/client/AIHubClient.d.ts +1058 -0
- package/dist/client/AIHubClient.d.ts.map +1 -0
- package/dist/client/AIHubClient.js +950 -0
- package/dist/client/AIHubClient.js.map +1 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +9 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/sseParser.d.ts +41 -0
- package/dist/client/sseParser.d.ts.map +1 -0
- package/dist/client/sseParser.js +153 -0
- package/dist/client/sseParser.js.map +1 -0
- package/dist/client/tokenStorage.d.ts +56 -0
- package/dist/client/tokenStorage.d.ts.map +1 -0
- package/dist/client/tokenStorage.js +116 -0
- package/dist/client/tokenStorage.js.map +1 -0
- package/dist/components/AgentsPicker/index.d.ts +70 -0
- package/dist/components/AgentsPicker/index.d.ts.map +1 -0
- package/dist/components/AgentsPicker/index.js +63 -0
- package/dist/components/AgentsPicker/index.js.map +1 -0
- package/dist/components/AuthGate/index.d.ts +79 -0
- package/dist/components/AuthGate/index.d.ts.map +1 -0
- package/dist/components/AuthGate/index.js +61 -0
- package/dist/components/AuthGate/index.js.map +1 -0
- package/dist/components/CacheBadge/index.d.ts +88 -0
- package/dist/components/CacheBadge/index.d.ts.map +1 -0
- package/dist/components/CacheBadge/index.js +127 -0
- package/dist/components/CacheBadge/index.js.map +1 -0
- package/dist/components/ChangePassword/index.d.ts +109 -0
- package/dist/components/ChangePassword/index.d.ts.map +1 -0
- package/dist/components/ChangePassword/index.js +142 -0
- package/dist/components/ChangePassword/index.js.map +1 -0
- package/dist/components/ChatShell/ShellHeader.d.ts +44 -0
- package/dist/components/ChatShell/ShellHeader.d.ts.map +1 -0
- package/dist/components/ChatShell/ShellHeader.js +14 -0
- package/dist/components/ChatShell/ShellHeader.js.map +1 -0
- package/dist/components/ChatShell/UserMenu.d.ts +48 -0
- package/dist/components/ChatShell/UserMenu.d.ts.map +1 -0
- package/dist/components/ChatShell/UserMenu.js +86 -0
- package/dist/components/ChatShell/UserMenu.js.map +1 -0
- package/dist/components/ChatShell/index.d.ts +98 -0
- package/dist/components/ChatShell/index.d.ts.map +1 -0
- package/dist/components/ChatShell/index.js +124 -0
- package/dist/components/ChatShell/index.js.map +1 -0
- package/dist/components/DataClubAIHub/index.d.ts +132 -0
- package/dist/components/DataClubAIHub/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHub/index.js +124 -0
- package/dist/components/DataClubAIHub/index.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts +89 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js +199 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts +33 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.js +137 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts +50 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.js +93 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts +49 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js +94 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts +37 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js +44 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts +56 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js +158 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts +29 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js +183 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts +38 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js +294 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts +36 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js +373 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts +47 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.js +233 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/index.d.ts +87 -0
- package/dist/components/DataClubAIHubAdmin/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/index.js +137 -0
- package/dist/components/DataClubAIHubAdmin/index.js.map +1 -0
- package/dist/components/DataClubAIHubLogin/index.d.ts +113 -0
- package/dist/components/DataClubAIHubLogin/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHubLogin/index.js +123 -0
- package/dist/components/DataClubAIHubLogin/index.js.map +1 -0
- package/dist/components/EffortControl/index.d.ts +57 -0
- package/dist/components/EffortControl/index.d.ts.map +1 -0
- package/dist/components/EffortControl/index.js +84 -0
- package/dist/components/EffortControl/index.js.map +1 -0
- package/dist/components/Layout/index.d.ts +63 -0
- package/dist/components/Layout/index.d.ts.map +1 -0
- package/dist/components/Layout/index.js +45 -0
- package/dist/components/Layout/index.js.map +1 -0
- package/dist/components/MainColumn/index.d.ts +69 -0
- package/dist/components/MainColumn/index.d.ts.map +1 -0
- package/dist/components/MainColumn/index.js +47 -0
- package/dist/components/MainColumn/index.js.map +1 -0
- package/dist/components/MarkdownRenderer/index.d.ts +66 -0
- package/dist/components/MarkdownRenderer/index.d.ts.map +1 -0
- package/dist/components/MarkdownRenderer/index.js +153 -0
- package/dist/components/MarkdownRenderer/index.js.map +1 -0
- package/dist/components/MessageInput/index.d.ts +85 -0
- package/dist/components/MessageInput/index.d.ts.map +1 -0
- package/dist/components/MessageInput/index.js +139 -0
- package/dist/components/MessageInput/index.js.map +1 -0
- package/dist/components/MessageList/index.d.ts +156 -0
- package/dist/components/MessageList/index.d.ts.map +1 -0
- package/dist/components/MessageList/index.js +352 -0
- package/dist/components/MessageList/index.js.map +1 -0
- package/dist/components/ReasoningSelector/index.d.ts +77 -0
- package/dist/components/ReasoningSelector/index.d.ts.map +1 -0
- package/dist/components/ReasoningSelector/index.js +156 -0
- package/dist/components/ReasoningSelector/index.js.map +1 -0
- package/dist/components/ReasoningView/index.d.ts +94 -0
- package/dist/components/ReasoningView/index.d.ts.map +1 -0
- package/dist/components/ReasoningView/index.js +83 -0
- package/dist/components/ReasoningView/index.js.map +1 -0
- package/dist/components/ScrollToPresentButton/index.d.ts +54 -0
- package/dist/components/ScrollToPresentButton/index.d.ts.map +1 -0
- package/dist/components/ScrollToPresentButton/index.js +44 -0
- package/dist/components/ScrollToPresentButton/index.js.map +1 -0
- package/dist/components/SessionsPanel/SessionTitleModal.d.ts +79 -0
- package/dist/components/SessionsPanel/SessionTitleModal.d.ts.map +1 -0
- package/dist/components/SessionsPanel/SessionTitleModal.js +102 -0
- package/dist/components/SessionsPanel/SessionTitleModal.js.map +1 -0
- package/dist/components/SessionsPanel/index.d.ts +100 -0
- package/dist/components/SessionsPanel/index.d.ts.map +1 -0
- package/dist/components/SessionsPanel/index.js +176 -0
- package/dist/components/SessionsPanel/index.js.map +1 -0
- package/dist/components/SystemNotice/index.d.ts +66 -0
- package/dist/components/SystemNotice/index.d.ts.map +1 -0
- package/dist/components/SystemNotice/index.js +81 -0
- package/dist/components/SystemNotice/index.js.map +1 -0
- package/dist/components/ThemeSwitcher/index.d.ts +66 -0
- package/dist/components/ThemeSwitcher/index.d.ts.map +1 -0
- package/dist/components/ThemeSwitcher/index.js +73 -0
- package/dist/components/ThemeSwitcher/index.js.map +1 -0
- package/dist/components/ToolCallView/formatContent.d.ts +36 -0
- package/dist/components/ToolCallView/formatContent.d.ts.map +1 -0
- package/dist/components/ToolCallView/formatContent.js +53 -0
- package/dist/components/ToolCallView/formatContent.js.map +1 -0
- package/dist/components/ToolCallView/index.d.ts +120 -0
- package/dist/components/ToolCallView/index.d.ts.map +1 -0
- package/dist/components/ToolCallView/index.js +104 -0
- package/dist/components/ToolCallView/index.js.map +1 -0
- package/dist/components/TpmBanner/index.d.ts +53 -0
- package/dist/components/TpmBanner/index.d.ts.map +1 -0
- package/dist/components/TpmBanner/index.js +116 -0
- package/dist/components/TpmBanner/index.js.map +1 -0
- package/dist/components/TurnStatusIndicator/index.d.ts +77 -0
- package/dist/components/TurnStatusIndicator/index.d.ts.map +1 -0
- package/dist/components/TurnStatusIndicator/index.js +122 -0
- package/dist/components/TurnStatusIndicator/index.js.map +1 -0
- package/dist/components/internal/PasswordInput.d.ts +62 -0
- package/dist/components/internal/PasswordInput.d.ts.map +1 -0
- package/dist/components/internal/PasswordInput.js +42 -0
- package/dist/components/internal/PasswordInput.js.map +1 -0
- package/dist/context/AIHubContext.d.ts +220 -0
- package/dist/context/AIHubContext.d.ts.map +1 -0
- package/dist/context/AIHubContext.js +19 -0
- package/dist/context/AIHubContext.js.map +1 -0
- package/dist/context/AIHubProvider.d.ts +75 -0
- package/dist/context/AIHubProvider.d.ts.map +1 -0
- package/dist/context/AIHubProvider.js +954 -0
- package/dist/context/AIHubProvider.js.map +1 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +9 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/theme.d.ts +43 -0
- package/dist/context/theme.d.ts.map +1 -0
- package/dist/context/theme.js +70 -0
- package/dist/context/theme.js.map +1 -0
- package/dist/context/useAIHub.d.ts +104 -0
- package/dist/context/useAIHub.d.ts.map +1 -0
- package/dist/context/useAIHub.js +63 -0
- package/dist/context/useAIHub.js.map +1 -0
- package/dist/context/useAIHubAuth.d.ts +44 -0
- package/dist/context/useAIHubAuth.d.ts.map +1 -0
- package/dist/context/useAIHubAuth.js +35 -0
- package/dist/context/useAIHubAuth.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/index.d.ts +99 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +97 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/affordances.d.ts +182 -0
- package/dist/state/affordances.d.ts.map +1 -0
- package/dist/state/affordances.js +189 -0
- package/dist/state/affordances.js.map +1 -0
- package/dist/state/stateReducer.d.ts +72 -0
- package/dist/state/stateReducer.d.ts.map +1 -0
- package/dist/state/stateReducer.js +122 -0
- package/dist/state/stateReducer.js.map +1 -0
- package/dist/stories/EffortControl.stories.d.ts +14 -0
- package/dist/stories/EffortControl.stories.d.ts.map +1 -0
- package/dist/stories/EffortControl.stories.js +43 -0
- package/dist/stories/EffortControl.stories.js.map +1 -0
- package/dist/stories/MarkdownRenderer.stories.d.ts +25 -0
- package/dist/stories/MarkdownRenderer.stories.d.ts.map +1 -0
- package/dist/stories/MarkdownRenderer.stories.js +178 -0
- package/dist/stories/MarkdownRenderer.stories.js.map +1 -0
- package/dist/stories/MessageBubble.stories.d.ts +21 -0
- package/dist/stories/MessageBubble.stories.d.ts.map +1 -0
- package/dist/stories/MessageBubble.stories.js +59 -0
- package/dist/stories/MessageBubble.stories.js.map +1 -0
- package/dist/stories/ReasoningView.stories.d.ts +18 -0
- package/dist/stories/ReasoningView.stories.d.ts.map +1 -0
- package/dist/stories/ReasoningView.stories.js +62 -0
- package/dist/stories/ReasoningView.stories.js.map +1 -0
- package/dist/stories/ScrollToPresentButton.stories.d.ts +18 -0
- package/dist/stories/ScrollToPresentButton.stories.d.ts.map +1 -0
- package/dist/stories/ScrollToPresentButton.stories.js +34 -0
- package/dist/stories/ScrollToPresentButton.stories.js.map +1 -0
- package/dist/stories/SystemNotice.stories.d.ts +17 -0
- package/dist/stories/SystemNotice.stories.d.ts.map +1 -0
- package/dist/stories/SystemNotice.stories.js +42 -0
- package/dist/stories/SystemNotice.stories.js.map +1 -0
- package/dist/stories/ThemeSwitcher.stories.d.ts +15 -0
- package/dist/stories/ThemeSwitcher.stories.d.ts.map +1 -0
- package/dist/stories/ThemeSwitcher.stories.js +35 -0
- package/dist/stories/ThemeSwitcher.stories.js.map +1 -0
- package/dist/stories/ToolCallView.stories.d.ts +20 -0
- package/dist/stories/ToolCallView.stories.d.ts.map +1 -0
- package/dist/stories/ToolCallView.stories.js +65 -0
- package/dist/stories/ToolCallView.stories.js.map +1 -0
- package/dist/stories/TpmBanner.stories.d.ts +24 -0
- package/dist/stories/TpmBanner.stories.d.ts.map +1 -0
- package/dist/stories/TpmBanner.stories.js +63 -0
- package/dist/stories/TpmBanner.stories.js.map +1 -0
- package/dist/stories/TurnStatusIndicator.stories.d.ts +25 -0
- package/dist/stories/TurnStatusIndicator.stories.d.ts.map +1 -0
- package/dist/stories/TurnStatusIndicator.stories.js +56 -0
- package/dist/stories/TurnStatusIndicator.stories.js.map +1 -0
- package/dist/stories/_mockProvider.d.ts +44 -0
- package/dist/stories/_mockProvider.d.ts.map +1 -0
- package/dist/stories/_mockProvider.js +84 -0
- package/dist/stories/_mockProvider.js.map +1 -0
- package/dist/types.d.ts +787 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/webcomponent/defineWebComponent.d.ts +106 -0
- package/dist/webcomponent/defineWebComponent.d.ts.map +1 -0
- package/dist/webcomponent/defineWebComponent.js +126 -0
- package/dist/webcomponent/defineWebComponent.js.map +1 -0
- package/dist/webcomponent/index.d.ts +7 -0
- package/dist/webcomponent/index.d.ts.map +1 -0
- package/dist/webcomponent/index.js +7 -0
- package/dist/webcomponent/index.js.map +1 -0
- package/package.json +63 -0
- package/src/__lint-canary__/step1-lint-canary.ts.disabled +28 -0
- package/src/auth/passwordPolicy.ts +75 -0
- package/src/auth/texts.ts +1294 -0
- package/src/client/AIHubClient.ts +1718 -0
- package/src/client/index.ts +30 -0
- package/src/client/sseParser.ts +168 -0
- package/src/client/tokenStorage.ts +131 -0
- package/src/components/AgentsPicker/index.tsx +147 -0
- package/src/components/AuthGate/index.tsx +132 -0
- package/src/components/CacheBadge/index.tsx +245 -0
- package/src/components/ChangePassword/index.tsx +375 -0
- package/src/components/ChatShell/ShellHeader.tsx +80 -0
- package/src/components/ChatShell/UserMenu.tsx +196 -0
- package/src/components/ChatShell/index.tsx +307 -0
- package/src/components/DataClubAIHub/index.tsx +300 -0
- package/src/components/DataClubAIHubAdmin/AgentEditorModal.tsx +505 -0
- package/src/components/DataClubAIHubAdmin/AgentsPage.tsx +342 -0
- package/src/components/DataClubAIHubAdmin/CreateUserModal.tsx +214 -0
- package/src/components/DataClubAIHubAdmin/PasswordRevealModal.tsx +204 -0
- package/src/components/DataClubAIHubAdmin/ResetPasswordModal.tsx +122 -0
- package/src/components/DataClubAIHubAdmin/RotatingMessageEditorModal.tsx +408 -0
- package/src/components/DataClubAIHubAdmin/RotatingMessagesPage.tsx +373 -0
- package/src/components/DataClubAIHubAdmin/SessionsOverviewPage.tsx +639 -0
- package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +979 -0
- package/src/components/DataClubAIHubAdmin/UsersPage.tsx +495 -0
- package/src/components/DataClubAIHubAdmin/index.tsx +277 -0
- package/src/components/DataClubAIHubLogin/index.tsx +282 -0
- package/src/components/EffortControl/index.tsx +138 -0
- package/src/components/Layout/index.tsx +92 -0
- package/src/components/MainColumn/index.tsx +131 -0
- package/src/components/MarkdownRenderer/index.tsx +256 -0
- package/src/components/MessageInput/index.tsx +283 -0
- package/src/components/MessageList/index.tsx +753 -0
- package/src/components/ReasoningSelector/index.tsx +268 -0
- package/src/components/ReasoningView/index.tsx +183 -0
- package/src/components/ScrollToPresentButton/index.tsx +85 -0
- package/src/components/SessionsPanel/SessionTitleModal.tsx +256 -0
- package/src/components/SessionsPanel/index.tsx +406 -0
- package/src/components/SystemNotice/index.tsx +151 -0
- package/src/components/ThemeSwitcher/index.tsx +136 -0
- package/src/components/ToolCallView/formatContent.ts +50 -0
- package/src/components/ToolCallView/index.tsx +274 -0
- package/src/components/TpmBanner/index.tsx +172 -0
- package/src/components/TurnStatusIndicator/index.tsx +203 -0
- package/src/components/internal/PasswordInput.tsx +145 -0
- package/src/context/AIHubContext.ts +249 -0
- package/src/context/AIHubProvider.tsx +1084 -0
- package/src/context/index.ts +9 -0
- package/src/context/theme.ts +76 -0
- package/src/context/useAIHub.ts +151 -0
- package/src/context/useAIHubAuth.ts +65 -0
- package/src/index.ts +275 -0
- package/src/primitives/index.ts +101 -0
- package/src/state/affordances.ts +321 -0
- package/src/state/stateReducer.ts +131 -0
- package/src/stories/EffortControl.stories.tsx +63 -0
- package/src/stories/MarkdownRenderer.stories.tsx +201 -0
- package/src/stories/MessageBubble.stories.tsx +101 -0
- package/src/stories/ReasoningView.stories.tsx +81 -0
- package/src/stories/ScrollToPresentButton.stories.tsx +52 -0
- package/src/stories/SystemNotice.stories.tsx +59 -0
- package/src/stories/ThemeSwitcher.stories.tsx +55 -0
- package/src/stories/ToolCallView.stories.tsx +95 -0
- package/src/stories/TpmBanner.stories.tsx +93 -0
- package/src/stories/TurnStatusIndicator.stories.tsx +86 -0
- package/src/stories/_mockProvider.tsx +116 -0
- package/src/styles/CLASSES.md +356 -0
- package/src/styles/default.css +2937 -0
- package/src/types.ts +867 -0
- package/src/webcomponent/defineWebComponent.tsx +193 -0
- package/src/webcomponent/index.ts +11 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AIHubClient` — the typed TypeScript SDK for `@data-club/ai-hub-server`.
|
|
3
|
+
*
|
|
4
|
+
* Used internally by `<AIHubProvider>` to power every component in the
|
|
5
|
+
* library; also exported for apps that build entirely custom UIs without
|
|
6
|
+
* the React layer (§7.8).
|
|
7
|
+
*
|
|
8
|
+
* Design notes:
|
|
9
|
+
*
|
|
10
|
+
* - **Wire is snake_case; SDK arguments are camelCase.** The translation is
|
|
11
|
+
* per-method and explicit so the reader can see exactly which fields cross
|
|
12
|
+
* the boundary. The only header-level camelCase translation is on
|
|
13
|
+
* `login()`'s response: `must_change_password` → `mustChangePassword`
|
|
14
|
+
* (per §6.3.4). `user.*` and `deployment.*` keep their snake_case keys —
|
|
15
|
+
* they're echoed through to React state and the BEM CSS contract is
|
|
16
|
+
* already snake_case-aware.
|
|
17
|
+
* - **SSE uses `fetch` + `ReadableStream`, not `EventSource`** (§9.5 — the
|
|
18
|
+
* browser API can't carry custom auth headers).
|
|
19
|
+
* - **No silent refresh** (§6.7). A 401 on any protected request fires the
|
|
20
|
+
* `onUnauthorized` callback exactly once; the provider then routes the
|
|
21
|
+
* user back to `<DataClubAIHubLogin />`.
|
|
22
|
+
* - **Empty-title rejection on `createSession`** is enforced client-side
|
|
23
|
+
* before the request goes out so we don't burn an HTTP round-trip on a
|
|
24
|
+
* case the server will 400 anyway.
|
|
25
|
+
*
|
|
26
|
+
* @see SPECIFICATIONS.md §7.8 — client SDK
|
|
27
|
+
* @see SPECIFICATIONS.md §6.3 — API surface
|
|
28
|
+
* @see SPECIFICATIONS.md §6.7 — auth model
|
|
29
|
+
*/
|
|
30
|
+
import { parseSseStream } from "./sseParser.js";
|
|
31
|
+
import { resolveTokenStorage, } from "./tokenStorage.js";
|
|
32
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
33
|
+
/* Internal helpers */
|
|
34
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
35
|
+
/**
|
|
36
|
+
* Thrown for any HTTP-level failure (non-2xx, network drop, etc.). The
|
|
37
|
+
* `code` discriminates by §7.2's `AIHubError` codes; the message is safe
|
|
38
|
+
* to render to end-users (it's either the server's `error.message` or a
|
|
39
|
+
* fixed transport-level string).
|
|
40
|
+
*/
|
|
41
|
+
export class AIHubRequestError extends Error {
|
|
42
|
+
/** Machine-readable code; clients branch on this rather than the message. */
|
|
43
|
+
name = "AIHubRequestError";
|
|
44
|
+
/** Discriminator matching `AIHubError.code`. */
|
|
45
|
+
code;
|
|
46
|
+
/** HTTP status when one was available. */
|
|
47
|
+
status;
|
|
48
|
+
/** Pino `request_id` from the server for log correlation. */
|
|
49
|
+
request_id;
|
|
50
|
+
/**
|
|
51
|
+
* @param code - the error discriminator.
|
|
52
|
+
* @param message - the human-readable message.
|
|
53
|
+
* @param status - HTTP status, when available.
|
|
54
|
+
* @param requestId - request id, when available.
|
|
55
|
+
*/
|
|
56
|
+
constructor(code, message, status, requestId) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
if (status !== undefined)
|
|
60
|
+
this.status = status;
|
|
61
|
+
if (requestId !== undefined)
|
|
62
|
+
this.request_id = requestId;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
66
|
+
/* AIHubClient */
|
|
67
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
68
|
+
/**
|
|
69
|
+
* The typed SDK every React surface in the library is built on.
|
|
70
|
+
*
|
|
71
|
+
* The client owns the JWT lifecycle (storage + clearing on 401) and the
|
|
72
|
+
* SSE plumbing; everything else is a thin HTTP wrapper that translates
|
|
73
|
+
* camelCase arguments into the server's snake_case wire format.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const client = new AIHubClient({ baseUrl: "/api/ai-hub" });
|
|
78
|
+
* const { token, mustChangePassword, user } = await client.login("alice", "secret");
|
|
79
|
+
* if (!mustChangePassword) {
|
|
80
|
+
* for await (const event of client.streamChat({
|
|
81
|
+
* sessionId: session.id,
|
|
82
|
+
* userMessage: "Hello.",
|
|
83
|
+
* })) {
|
|
84
|
+
* console.log(event);
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export class AIHubClient {
|
|
90
|
+
baseUrl;
|
|
91
|
+
fetchImpl;
|
|
92
|
+
tokenStore;
|
|
93
|
+
onUnauthorized;
|
|
94
|
+
/** Guards against firing `onUnauthorized` more than once per 401 storm. */
|
|
95
|
+
unauthorizedFired = false;
|
|
96
|
+
/**
|
|
97
|
+
* @param options - constructor options. `baseUrl` is required; everything
|
|
98
|
+
* else has a sensible default.
|
|
99
|
+
*/
|
|
100
|
+
constructor(options) {
|
|
101
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
102
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
103
|
+
this.tokenStore = resolveTokenStorage(options.tokenStorage ?? "localStorage");
|
|
104
|
+
if (options.onUnauthorized)
|
|
105
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
106
|
+
}
|
|
107
|
+
/* ───── token surface ──────────────────────────────────────────────── */
|
|
108
|
+
/** Returns the currently-stored token, or null. */
|
|
109
|
+
getToken() {
|
|
110
|
+
return this.tokenStore.get();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Replaces the stored token. Used by the provider after `login` /
|
|
114
|
+
* `changePassword` to persist the JWT before the next request fires.
|
|
115
|
+
*
|
|
116
|
+
* @param token - the bearer JWT to store.
|
|
117
|
+
*/
|
|
118
|
+
setToken(token) {
|
|
119
|
+
this.tokenStore.set(token);
|
|
120
|
+
this.unauthorizedFired = false;
|
|
121
|
+
}
|
|
122
|
+
/** Clears the stored token. Idempotent. */
|
|
123
|
+
clearToken() {
|
|
124
|
+
this.tokenStore.clear();
|
|
125
|
+
}
|
|
126
|
+
/* ───── auth ───────────────────────────────────────────────────────── */
|
|
127
|
+
/**
|
|
128
|
+
* `POST /auth/login`. On success, persists the returned token automatically
|
|
129
|
+
* so subsequent calls authenticate without the caller threading the JWT
|
|
130
|
+
* through.
|
|
131
|
+
*
|
|
132
|
+
* @param username - the user's login id.
|
|
133
|
+
* @param password - plaintext password (sent over HTTPS to the server).
|
|
134
|
+
* @returns the login response, with `mustChangePassword` camelcased.
|
|
135
|
+
*/
|
|
136
|
+
async login(username, password) {
|
|
137
|
+
const raw = await this.requestJson("POST", "/auth/login", {
|
|
138
|
+
body: { username, password },
|
|
139
|
+
authenticated: false,
|
|
140
|
+
});
|
|
141
|
+
this.setToken(raw.token);
|
|
142
|
+
const response = {
|
|
143
|
+
token: raw.token,
|
|
144
|
+
mustChangePassword: raw.must_change_password,
|
|
145
|
+
user: raw.user,
|
|
146
|
+
};
|
|
147
|
+
if (raw.deployment)
|
|
148
|
+
response.deployment = raw.deployment;
|
|
149
|
+
return response;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `POST /auth/change-password`. Returns the fresh session token (the
|
|
153
|
+
* change-only token is consumed by this call).
|
|
154
|
+
*
|
|
155
|
+
* @param currentPassword - the user's existing password, or null when
|
|
156
|
+
* the caller is authenticating with a password-change-only token
|
|
157
|
+
* (forced-change flow — §8.5).
|
|
158
|
+
* @param newPassword - the new password. Must satisfy the §8.5 policy
|
|
159
|
+
* (≥8 chars, ≥2 of {letters, digits, special}).
|
|
160
|
+
* @returns the new session token, persisted automatically.
|
|
161
|
+
*/
|
|
162
|
+
async changePassword(currentPassword, newPassword) {
|
|
163
|
+
const body = { new_password: newPassword };
|
|
164
|
+
if (currentPassword !== null)
|
|
165
|
+
body.current_password = currentPassword;
|
|
166
|
+
const result = await this.requestJson("POST", "/auth/change-password", { body });
|
|
167
|
+
this.setToken(result.token);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* `POST /auth/logout`. Server-side this is a no-op in v1 (no token
|
|
172
|
+
* blacklist — §6.3.4); we issue it for symmetry and to give the server
|
|
173
|
+
* an audit-log entry. Always clears the local token afterward, even
|
|
174
|
+
* when the request fails.
|
|
175
|
+
*/
|
|
176
|
+
async logout() {
|
|
177
|
+
try {
|
|
178
|
+
await this.requestJson("POST", "/auth/logout", { body: {} });
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
/* network errors here are not fatal — clear and move on */
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
this.clearToken();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* `GET /auth/me` — fetches the `{ user, deployment }` envelope.
|
|
189
|
+
*/
|
|
190
|
+
async me() {
|
|
191
|
+
return this.requestJson("GET", "/auth/me");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `GET /feature-toggles` — fetches the deployment-wide UI feature
|
|
195
|
+
* toggles set by `super_admin` (§8.4). **Unauthenticated** — no token
|
|
196
|
+
* required, every role gets the same response. The provider calls this
|
|
197
|
+
* on mount so the §8.4 precedence ("server toggle > React prop >
|
|
198
|
+
* built-in default") applies before the chat shell first renders.
|
|
199
|
+
*
|
|
200
|
+
* Missing keys are returned absent in the response (not coerced to
|
|
201
|
+
* `false`) so the client can distinguish "explicitly off" from "fall
|
|
202
|
+
* through to the React prop default."
|
|
203
|
+
*
|
|
204
|
+
* @returns the wire-format toggles blob; may be empty (`{}`) when the
|
|
205
|
+
* super_admin hasn't set anything.
|
|
206
|
+
*/
|
|
207
|
+
async getFeatureToggles() {
|
|
208
|
+
const result = await this.requestJson("GET", "/feature-toggles");
|
|
209
|
+
return result.feature_toggles ?? {};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* `PATCH /auth/me/preferences` — updates the caller's stored
|
|
213
|
+
* `theme_preference`. Returns the refreshed envelope.
|
|
214
|
+
*
|
|
215
|
+
* Rejected with `AIHubRequestError(code: "theme_switcher_disabled")` (HTTP
|
|
216
|
+
* 409) when the deployment has the switcher turned off (§7.6).
|
|
217
|
+
*
|
|
218
|
+
* @param prefs - the preferences patch (currently only `theme`).
|
|
219
|
+
* @param prefs.theme - new theme preference (`"light"` or `"dark"`).
|
|
220
|
+
*/
|
|
221
|
+
async updatePreferences(prefs) {
|
|
222
|
+
return this.requestJson("PATCH", "/auth/me/preferences", { body: prefs });
|
|
223
|
+
}
|
|
224
|
+
/* ───── sessions ───────────────────────────────────────────────────── */
|
|
225
|
+
/**
|
|
226
|
+
* `GET /sessions?agent_id=...&include_archived=...`.
|
|
227
|
+
*
|
|
228
|
+
* @param options - filter arguments; `agentId` is required.
|
|
229
|
+
*/
|
|
230
|
+
async listSessions(options) {
|
|
231
|
+
const query = new URLSearchParams({ agent_id: options.agentId });
|
|
232
|
+
if (options.includeArchived)
|
|
233
|
+
query.set("include_archived", "true");
|
|
234
|
+
const result = await this.requestJson("GET", `/sessions?${query.toString()}`);
|
|
235
|
+
return result.sessions;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `POST /sessions`.
|
|
239
|
+
*
|
|
240
|
+
* Client-side validation: title must be 1..80 chars. Empty / oversize
|
|
241
|
+
* titles throw `AIHubRequestError(code: "validation")` synchronously
|
|
242
|
+
* without an HTTP round-trip.
|
|
243
|
+
*
|
|
244
|
+
* @param options - the agent id + non-empty title.
|
|
245
|
+
* @returns the created session row.
|
|
246
|
+
*/
|
|
247
|
+
async createSession(options) {
|
|
248
|
+
const title = options.title;
|
|
249
|
+
if (typeof title !== "string" || title.length === 0) {
|
|
250
|
+
throw new AIHubRequestError("validation", "Session title is required (1..80 characters).");
|
|
251
|
+
}
|
|
252
|
+
if (title.length > 80) {
|
|
253
|
+
throw new AIHubRequestError("validation", "Session title must be 80 characters or fewer.");
|
|
254
|
+
}
|
|
255
|
+
return this.requestJson("POST", "/sessions", {
|
|
256
|
+
body: { agent_id: options.agentId, title },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** `GET /sessions/:id`. */
|
|
260
|
+
async getSession(id) {
|
|
261
|
+
return this.requestJson("GET", `/sessions/${encodeURIComponent(id)}`);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* `PATCH /sessions/:id` — rename and/or archive in one call. An empty
|
|
265
|
+
* patch (`{}`) returns the row unchanged.
|
|
266
|
+
*
|
|
267
|
+
* @param id - the session id.
|
|
268
|
+
* @param patch - the fields to update.
|
|
269
|
+
*/
|
|
270
|
+
async patchSession(id, patch) {
|
|
271
|
+
const body = {};
|
|
272
|
+
if (patch.title !== undefined)
|
|
273
|
+
body.title = patch.title;
|
|
274
|
+
if (patch.isArchived !== undefined)
|
|
275
|
+
body.is_archived = patch.isArchived;
|
|
276
|
+
return this.requestJson("PATCH", `/sessions/${encodeURIComponent(id)}`, { body });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `PATCH /sessions/:id/reasoning` — update the user's per-session reasoning
|
|
280
|
+
* settings. The server applies them to the agent's `model_settings`
|
|
281
|
+
* automatically before the next chat turn.
|
|
282
|
+
*
|
|
283
|
+
* Returns the refreshed session including `capabilities`, so the caller
|
|
284
|
+
* can re-render UI controls without a separate GET.
|
|
285
|
+
*/
|
|
286
|
+
async patchSessionReasoning(id, patch) {
|
|
287
|
+
const body = {};
|
|
288
|
+
if (patch.effort !== undefined)
|
|
289
|
+
body.effort = patch.effort;
|
|
290
|
+
if (patch.verbosity !== undefined)
|
|
291
|
+
body.verbosity = patch.verbosity;
|
|
292
|
+
if (patch.thinkingBudget !== undefined)
|
|
293
|
+
body.thinking_budget = patch.thinkingBudget;
|
|
294
|
+
return this.requestJson("PATCH", `/sessions/${encodeURIComponent(id)}/reasoning`, { body });
|
|
295
|
+
}
|
|
296
|
+
/** `GET /sessions/:id/messages`. */
|
|
297
|
+
async getSessionMessages(id) {
|
|
298
|
+
const result = await this.requestJson("GET", `/sessions/${encodeURIComponent(id)}/messages`);
|
|
299
|
+
return result.messages;
|
|
300
|
+
}
|
|
301
|
+
/* ───── message overrides ──────────────────────────────────────────── */
|
|
302
|
+
/**
|
|
303
|
+
* `GET /sessions/:id/message-overrides` — returns the caller's per-message
|
|
304
|
+
* content substitutions for this session's underlying Letta conversation.
|
|
305
|
+
*
|
|
306
|
+
* Overrides are keyed by `ordinal_position` (0-based index into Letta's
|
|
307
|
+
* chronologically-sorted message list). They're applied automatically by
|
|
308
|
+
* `getSessionMessages` — callers don't need to merge manually; this
|
|
309
|
+
* endpoint exists so custom Tier-3 UIs can render an edit affordance
|
|
310
|
+
* next to each message that has an override.
|
|
311
|
+
*
|
|
312
|
+
* @param sessionId - the ai-hub session id.
|
|
313
|
+
* @returns the overrides sorted by `ordinal_position` ascending.
|
|
314
|
+
*/
|
|
315
|
+
async listMessageOverrides(sessionId) {
|
|
316
|
+
const result = await this.requestJson("GET", `/sessions/${encodeURIComponent(sessionId)}/message-overrides`);
|
|
317
|
+
return result.overrides;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* `PUT /sessions/:id/message-overrides/:position` — upsert one or both
|
|
321
|
+
* override layers for the message at `ordinalPosition`.
|
|
322
|
+
*
|
|
323
|
+
* Pass `systemOverride` to set the LLM-facing view, `userOverride` to
|
|
324
|
+
* set the UI-facing view, or both. At least one must be provided.
|
|
325
|
+
* **Merge semantics**: only the layer(s) you pass are written; an
|
|
326
|
+
* existing value on the other layer is preserved.
|
|
327
|
+
*
|
|
328
|
+
* **Validation gates** (server-enforced):
|
|
329
|
+
*
|
|
330
|
+
* - `409 turn_in_progress` — a turn is currently live for this session.
|
|
331
|
+
* - `409 message_not_found` — no Letta message exists at this position
|
|
332
|
+
* yet (typical when a tier-3 caller tries to override a message
|
|
333
|
+
* before it has landed; wait for the turn to finish first).
|
|
334
|
+
*
|
|
335
|
+
* Pass empty string for `userOverride` to visually hide the message
|
|
336
|
+
* (the bubble slot stays — use `deleteMessageOverride` to remove the
|
|
337
|
+
* slot entirely).
|
|
338
|
+
*
|
|
339
|
+
* @param sessionId - the ai-hub session id.
|
|
340
|
+
* @param ordinalPosition - the 0-based index in Letta's message list.
|
|
341
|
+
* @param override - one or both layers to write.
|
|
342
|
+
* @returns the resulting override row.
|
|
343
|
+
*/
|
|
344
|
+
async setMessageOverride(sessionId, ordinalPosition, override) {
|
|
345
|
+
const body = {};
|
|
346
|
+
if (override.systemOverride !== undefined)
|
|
347
|
+
body.system_override = override.systemOverride;
|
|
348
|
+
if (override.userOverride !== undefined)
|
|
349
|
+
body.user_override = override.userOverride;
|
|
350
|
+
return this.requestJson("PUT", `/sessions/${encodeURIComponent(sessionId)}/message-overrides/${ordinalPosition}`, { body });
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* `DELETE /sessions/:id/message-overrides/:position` — remove an override.
|
|
354
|
+
*
|
|
355
|
+
* Idempotent: returns successfully whether or not the row existed.
|
|
356
|
+
*
|
|
357
|
+
* @param sessionId - the ai-hub session id.
|
|
358
|
+
* @param ordinalPosition - the 0-based index in Letta's message list.
|
|
359
|
+
*/
|
|
360
|
+
async deleteMessageOverride(sessionId, ordinalPosition) {
|
|
361
|
+
await this.requestJson("DELETE", `/sessions/${encodeURIComponent(sessionId)}/message-overrides/${ordinalPosition}`);
|
|
362
|
+
}
|
|
363
|
+
/* ───── agents ─────────────────────────────────────────────────────── */
|
|
364
|
+
/**
|
|
365
|
+
* `GET /agents` — returns the user-facing agent catalog (non-archived
|
|
366
|
+
* only) with each entry's `session_count` scoped to the caller.
|
|
367
|
+
*/
|
|
368
|
+
async listAgents() {
|
|
369
|
+
const result = await this.requestJson("GET", "/agents");
|
|
370
|
+
return result.agents;
|
|
371
|
+
}
|
|
372
|
+
/* ───── chat ───────────────────────────────────────────────────────── */
|
|
373
|
+
/**
|
|
374
|
+
* `POST /chat` — opens an SSE stream. Returns an async iterable of
|
|
375
|
+
* `AIHubEvent`s; consume with `for await (const event of …)`.
|
|
376
|
+
*
|
|
377
|
+
* The iterable terminates when the server emits `stream_end` (the bytes
|
|
378
|
+
* stop after that). To abort early, pass `opts.signal`.
|
|
379
|
+
*
|
|
380
|
+
* @param body - the request body (sessionId, userMessage, optional effort,
|
|
381
|
+
* optional idempotencyKey).
|
|
382
|
+
* @param opts - optional abort signal and resumption hint.
|
|
383
|
+
*/
|
|
384
|
+
streamChat(body, opts = {}) {
|
|
385
|
+
const wireBody = {
|
|
386
|
+
session_id: body.sessionId,
|
|
387
|
+
user_message: body.userMessage,
|
|
388
|
+
};
|
|
389
|
+
if (body.effort !== undefined)
|
|
390
|
+
wireBody.effort = body.effort;
|
|
391
|
+
if (body.idempotencyKey !== undefined) {
|
|
392
|
+
wireBody.idempotency_key = body.idempotencyKey;
|
|
393
|
+
}
|
|
394
|
+
if (body.userOverride !== undefined) {
|
|
395
|
+
wireBody.user_override = body.userOverride;
|
|
396
|
+
}
|
|
397
|
+
return this.streamRequest("POST", "/chat", wireBody, opts);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* `GET /chat/:turn_id/events?from_seq=N` — resumption replay then
|
|
401
|
+
* live-attach.
|
|
402
|
+
*
|
|
403
|
+
* @param turnId - the turn to resume.
|
|
404
|
+
* @param opts - optional abort signal and `fromSeq` cursor (defaults to
|
|
405
|
+
* `-1` so every persisted event is replayed).
|
|
406
|
+
*/
|
|
407
|
+
getTurnEvents(turnId, opts = {}) {
|
|
408
|
+
const fromSeq = opts.fromSeq ?? -1;
|
|
409
|
+
const path = `/chat/${encodeURIComponent(turnId)}/events?from_seq=${encodeURIComponent(String(fromSeq))}`;
|
|
410
|
+
return this.streamRequest("GET", path, undefined, opts);
|
|
411
|
+
}
|
|
412
|
+
/** `POST /chat/:turn_id/cancel`. */
|
|
413
|
+
async cancelTurn(turnId) {
|
|
414
|
+
return this.requestJson("POST", `/chat/${encodeURIComponent(turnId)}/cancel`, { body: {} });
|
|
415
|
+
}
|
|
416
|
+
/* ───── admin: users ───────────────────────────────────────────────── */
|
|
417
|
+
/**
|
|
418
|
+
* `GET /admin/users` — fetches every user row in the deployment.
|
|
419
|
+
*
|
|
420
|
+
* Caller must be `admin` or `super_admin`; the server returns 403
|
|
421
|
+
* otherwise (the SDK surfaces it as `AIHubRequestError(code: "forbidden")`).
|
|
422
|
+
*
|
|
423
|
+
* @returns the user rows, sorted alphabetically by username.
|
|
424
|
+
* @throws {AIHubRequestError} `forbidden` for `user` / `power_user` callers.
|
|
425
|
+
*/
|
|
426
|
+
async listAdminUsers() {
|
|
427
|
+
const result = await this.requestJson("GET", "/admin/users");
|
|
428
|
+
return result.users;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* `POST /admin/users` — creates a user; the server generates the password.
|
|
432
|
+
*
|
|
433
|
+
* Returns the new row plus the **one-time** plaintext password
|
|
434
|
+
* (`generatedPassword`). The caller is responsible for surfacing it
|
|
435
|
+
* exactly once in the UI — there is no way to retrieve it later.
|
|
436
|
+
*
|
|
437
|
+
* Admins cannot create `super_admin`s (server returns 403 if the body
|
|
438
|
+
* requests it). On username collision returns 409 `username_taken`.
|
|
439
|
+
*
|
|
440
|
+
* @param body - the new user's identity + initial role.
|
|
441
|
+
* @returns the created row and the one-time password.
|
|
442
|
+
* @throws {AIHubRequestError} `forbidden` (caller role insufficient or
|
|
443
|
+
* admin tried to grant super_admin), `validation` (malformed body),
|
|
444
|
+
* or `username_taken` (409).
|
|
445
|
+
*/
|
|
446
|
+
async createAdminUser(body) {
|
|
447
|
+
const wireBody = { username: body.username };
|
|
448
|
+
if (body.displayName !== undefined) {
|
|
449
|
+
wireBody.display_name = body.displayName;
|
|
450
|
+
}
|
|
451
|
+
if (body.role !== undefined)
|
|
452
|
+
wireBody.role = body.role;
|
|
453
|
+
const result = await this.requestJson("POST", "/admin/users", { body: wireBody });
|
|
454
|
+
return {
|
|
455
|
+
user: result.user,
|
|
456
|
+
generatedPassword: result.generated_password,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* `PATCH /admin/users/:id/role` — changes a user's role.
|
|
461
|
+
*
|
|
462
|
+
* The server enforces the admin-shield + super-admin-min-count rules
|
|
463
|
+
* transactionally (§8.1). Violations come back as 403 with codes like
|
|
464
|
+
* `admin_shield`, `super_admin_min_count`, or `forbidden`.
|
|
465
|
+
*
|
|
466
|
+
* @param id - the target user's id.
|
|
467
|
+
* @param role - the new role.
|
|
468
|
+
* @returns the updated row.
|
|
469
|
+
* @throws {AIHubRequestError} `forbidden` (shield / min-count / caller
|
|
470
|
+
* cannot grant this role).
|
|
471
|
+
*/
|
|
472
|
+
async patchAdminUserRole(id, role) {
|
|
473
|
+
const result = await this.requestJson("PATCH", `/admin/users/${encodeURIComponent(id)}/role`, { body: { role } });
|
|
474
|
+
return result.user;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* `POST /admin/users/:id/reset-password` — issues a fresh password.
|
|
478
|
+
*
|
|
479
|
+
* Sets `is_initial_password = true` on the target. The new plaintext is
|
|
480
|
+
* returned **exactly once** in `generatedPassword`; the admin UI must
|
|
481
|
+
* surface it via the one-time-reveal modal.
|
|
482
|
+
*
|
|
483
|
+
* @param id - the target user's id.
|
|
484
|
+
* @returns the one-time plaintext.
|
|
485
|
+
* @throws {AIHubRequestError} `forbidden` (shield / min-count).
|
|
486
|
+
*/
|
|
487
|
+
async resetAdminUserPassword(id) {
|
|
488
|
+
const result = await this.requestJson("POST", `/admin/users/${encodeURIComponent(id)}/reset-password`, { body: {} });
|
|
489
|
+
return { generatedPassword: result.generated_password };
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* `DELETE /admin/users/:id` — hard-deletes a user.
|
|
493
|
+
*
|
|
494
|
+
* Cascades to that user's sessions via the FK. The server blocks
|
|
495
|
+
* self-delete (returns 403) so a careless click can't brick the caller's
|
|
496
|
+
* own account; shield + min-count apply as for role change.
|
|
497
|
+
*
|
|
498
|
+
* @param id - the target user's id.
|
|
499
|
+
* @throws {AIHubRequestError} `forbidden` (shield, min-count, or
|
|
500
|
+
* attempting to delete oneself).
|
|
501
|
+
*/
|
|
502
|
+
async deleteAdminUser(id) {
|
|
503
|
+
await this.requestJson("DELETE", `/admin/users/${encodeURIComponent(id)}`);
|
|
504
|
+
}
|
|
505
|
+
/* ───── admin: agents ──────────────────────────────────────────────── */
|
|
506
|
+
/**
|
|
507
|
+
* `GET /admin/agents` — every ai-hub agent in the deployment, including
|
|
508
|
+
* archived ones. Caller must be `power_user`+ per §8.2.
|
|
509
|
+
*
|
|
510
|
+
* Sort order: ascending by `display_name` (the server's `ORDER BY`).
|
|
511
|
+
*
|
|
512
|
+
* @returns the agent rows.
|
|
513
|
+
* @throws {AIHubRequestError} `forbidden` for `user` callers.
|
|
514
|
+
*/
|
|
515
|
+
async listAdminAgents() {
|
|
516
|
+
const result = await this.requestJson("GET", "/admin/agents");
|
|
517
|
+
return result.agents;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* `POST /admin/agents` — register a new ai-hub agent in the catalog.
|
|
521
|
+
*
|
|
522
|
+
* The server validates `lettaAgentId` against Letta; if the id isn't
|
|
523
|
+
* resolvable the response is 400 `letta_agent_not_found` (surface inline
|
|
524
|
+
* on the form field).
|
|
525
|
+
*
|
|
526
|
+
* @param body - the display name + assigned Letta agent id.
|
|
527
|
+
* @returns the newly-created row.
|
|
528
|
+
* @throws {AIHubRequestError} `letta_agent_not_found`, `validation`, `forbidden`.
|
|
529
|
+
*/
|
|
530
|
+
async createAdminAgent(body) {
|
|
531
|
+
const wire = {
|
|
532
|
+
display_name: body.displayName,
|
|
533
|
+
letta_agent_id: body.lettaAgentId,
|
|
534
|
+
};
|
|
535
|
+
if (body.sandboxSeedName !== undefined) {
|
|
536
|
+
wire.sandbox_seed_name = body.sandboxSeedName;
|
|
537
|
+
}
|
|
538
|
+
if (body.forkSourceConversationId !== undefined) {
|
|
539
|
+
wire.fork_source_conversation_id = body.forkSourceConversationId;
|
|
540
|
+
}
|
|
541
|
+
return this.requestJson("POST", "/admin/agents", {
|
|
542
|
+
body: wire,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* `PATCH /admin/agents/:id` — update one or more fields. Every field is
|
|
547
|
+
* optional; an empty patch is a 200 no-op.
|
|
548
|
+
*
|
|
549
|
+
* Archive / unarchive flows through this same endpoint via `isArchived`.
|
|
550
|
+
*
|
|
551
|
+
* @param id - the ai-hub agent id.
|
|
552
|
+
* @param body - the fields to update.
|
|
553
|
+
* @returns the refreshed row.
|
|
554
|
+
* @throws {AIHubRequestError} `letta_agent_not_found` if `lettaAgentId`
|
|
555
|
+
* was supplied and doesn't resolve in Letta.
|
|
556
|
+
*/
|
|
557
|
+
async patchAdminAgent(id, body) {
|
|
558
|
+
const wire = {};
|
|
559
|
+
if (body.displayName !== undefined)
|
|
560
|
+
wire.display_name = body.displayName;
|
|
561
|
+
if (body.lettaAgentId !== undefined)
|
|
562
|
+
wire.letta_agent_id = body.lettaAgentId;
|
|
563
|
+
if (body.isArchived !== undefined)
|
|
564
|
+
wire.is_archived = body.isArchived;
|
|
565
|
+
if (body.sandboxSeedName !== undefined) {
|
|
566
|
+
wire.sandbox_seed_name = body.sandboxSeedName;
|
|
567
|
+
}
|
|
568
|
+
if (body.forkSourceConversationId !== undefined) {
|
|
569
|
+
wire.fork_source_conversation_id = body.forkSourceConversationId;
|
|
570
|
+
}
|
|
571
|
+
return this.requestJson("PATCH", `/admin/agents/${encodeURIComponent(id)}`, { body: wire });
|
|
572
|
+
}
|
|
573
|
+
/* ───── admin: rotating messages ────────────────────────────────────── */
|
|
574
|
+
/**
|
|
575
|
+
* `GET /admin/rotating-messages` — every rotating starting message in the
|
|
576
|
+
* deployment (archived included), sorted by display name. `power_user`+.
|
|
577
|
+
*
|
|
578
|
+
* @returns the rows.
|
|
579
|
+
* @throws {AIHubRequestError} `forbidden` for `user` callers.
|
|
580
|
+
*/
|
|
581
|
+
async listAdminRotatingMessages() {
|
|
582
|
+
const result = await this.requestJson("GET", "/admin/rotating-messages");
|
|
583
|
+
return result.rotating_messages;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* `POST /admin/rotating-messages` — create a rotating starting message. The
|
|
587
|
+
* server validates `lettaAgentId` against Letta (`letta_agent_not_found`)
|
|
588
|
+
* and the schedule shape (`invalid_schedule`).
|
|
589
|
+
*
|
|
590
|
+
* @param body - the prompt, agent, and schedule config.
|
|
591
|
+
* @returns the newly-created row (including its `rmsg-…` id).
|
|
592
|
+
* @throws {AIHubRequestError} `letta_agent_not_found`, `invalid_schedule`, `validation`, `forbidden`.
|
|
593
|
+
*/
|
|
594
|
+
async createAdminRotatingMessage(body) {
|
|
595
|
+
const wire = {
|
|
596
|
+
display_name: body.displayName,
|
|
597
|
+
prompt: body.prompt,
|
|
598
|
+
letta_agent_id: body.lettaAgentId,
|
|
599
|
+
schedule_kind: body.scheduleKind,
|
|
600
|
+
};
|
|
601
|
+
if (body.intervalSeconds !== undefined)
|
|
602
|
+
wire.interval_seconds = body.intervalSeconds;
|
|
603
|
+
if (body.dailyTime !== undefined)
|
|
604
|
+
wire.daily_time = body.dailyTime;
|
|
605
|
+
if (body.cronExpression !== undefined)
|
|
606
|
+
wire.cron_expression = body.cronExpression;
|
|
607
|
+
return this.requestJson("POST", "/admin/rotating-messages", { body: wire });
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* `PATCH /admin/rotating-messages/:id` — update one or more fields. Every
|
|
611
|
+
* field is optional; an empty patch is a 200 no-op. Changing the schedule
|
|
612
|
+
* recomputes the next rotation time.
|
|
613
|
+
*
|
|
614
|
+
* @param id - the rotating message id.
|
|
615
|
+
* @param body - the fields to update.
|
|
616
|
+
* @returns the refreshed row.
|
|
617
|
+
* @throws {AIHubRequestError} `letta_agent_not_found`, `invalid_schedule`, `not_found`, `forbidden`.
|
|
618
|
+
*/
|
|
619
|
+
async patchAdminRotatingMessage(id, body) {
|
|
620
|
+
const wire = {};
|
|
621
|
+
if (body.displayName !== undefined)
|
|
622
|
+
wire.display_name = body.displayName;
|
|
623
|
+
if (body.prompt !== undefined)
|
|
624
|
+
wire.prompt = body.prompt;
|
|
625
|
+
if (body.lettaAgentId !== undefined)
|
|
626
|
+
wire.letta_agent_id = body.lettaAgentId;
|
|
627
|
+
if (body.scheduleKind !== undefined)
|
|
628
|
+
wire.schedule_kind = body.scheduleKind;
|
|
629
|
+
if (body.intervalSeconds !== undefined)
|
|
630
|
+
wire.interval_seconds = body.intervalSeconds;
|
|
631
|
+
if (body.dailyTime !== undefined)
|
|
632
|
+
wire.daily_time = body.dailyTime;
|
|
633
|
+
if (body.cronExpression !== undefined)
|
|
634
|
+
wire.cron_expression = body.cronExpression;
|
|
635
|
+
if (body.isArchived !== undefined)
|
|
636
|
+
wire.is_archived = body.isArchived;
|
|
637
|
+
return this.requestJson("PATCH", `/admin/rotating-messages/${encodeURIComponent(id)}`, { body: wire });
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* `POST /admin/rotating-messages/:id/rotate-now` — run a rotation inline
|
|
641
|
+
* (preview / force-refresh). May take a while — it spawns the Letta Code
|
|
642
|
+
* subprocess like a real chat turn.
|
|
643
|
+
*
|
|
644
|
+
* @param id - the rotating message id.
|
|
645
|
+
* @returns the refreshed row plus the attempt's outcome.
|
|
646
|
+
* @throws {AIHubRequestError} `not_found`, `forbidden`.
|
|
647
|
+
*/
|
|
648
|
+
async rotateAdminRotatingMessageNow(id) {
|
|
649
|
+
return this.requestJson("POST", `/admin/rotating-messages/${encodeURIComponent(id)}/rotate-now`, { body: {} });
|
|
650
|
+
}
|
|
651
|
+
/* ───── admin: sessions overview ───────────────────────────────────── */
|
|
652
|
+
/**
|
|
653
|
+
* `GET /admin/sessions` — paginated read-only browser over every session
|
|
654
|
+
* in the deployment. `power_user`+ per §8.2.
|
|
655
|
+
*
|
|
656
|
+
* Each row carries `owner_username` + `agent_display_name` joined
|
|
657
|
+
* server-side. Use `nextCursor` (when non-null) as the `cursor` for the
|
|
658
|
+
* next page.
|
|
659
|
+
*
|
|
660
|
+
* @param options - filter + pagination args. All optional.
|
|
661
|
+
* @returns the page plus the next-cursor.
|
|
662
|
+
*/
|
|
663
|
+
async listAdminSessions(options = {}) {
|
|
664
|
+
const query = new URLSearchParams();
|
|
665
|
+
if (options.ownerId !== undefined)
|
|
666
|
+
query.set("owner_id", options.ownerId);
|
|
667
|
+
if (options.agentId !== undefined)
|
|
668
|
+
query.set("agent_id", options.agentId);
|
|
669
|
+
if (options.status !== undefined)
|
|
670
|
+
query.set("status", options.status);
|
|
671
|
+
if (options.search !== undefined && options.search.length > 0) {
|
|
672
|
+
query.set("search", options.search);
|
|
673
|
+
}
|
|
674
|
+
if (options.cursor !== undefined)
|
|
675
|
+
query.set("cursor", options.cursor);
|
|
676
|
+
if (options.limit !== undefined)
|
|
677
|
+
query.set("limit", String(options.limit));
|
|
678
|
+
const qs = query.toString();
|
|
679
|
+
const path = qs.length > 0 ? `/admin/sessions?${qs}` : "/admin/sessions";
|
|
680
|
+
const result = await this.requestJson("GET", path);
|
|
681
|
+
return { sessions: result.sessions, nextCursor: result.next_cursor };
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* `GET /admin/sessions/:id/messages` — read-only history for any session
|
|
685
|
+
* (cross-user). Returns the same `AIHubMessage[]` shape as the user-facing
|
|
686
|
+
* `getSessionMessages` so the same `<MessageBubble />` rendering can be
|
|
687
|
+
* reused.
|
|
688
|
+
*
|
|
689
|
+
* @param id - any session id in the deployment.
|
|
690
|
+
* @returns the projected message list.
|
|
691
|
+
* @throws {AIHubRequestError} `forbidden` for `user` callers, `not_found`
|
|
692
|
+
* if the session id doesn't exist.
|
|
693
|
+
*/
|
|
694
|
+
async getAdminSessionMessages(id) {
|
|
695
|
+
const result = await this.requestJson("GET", `/admin/sessions/${encodeURIComponent(id)}/messages`);
|
|
696
|
+
return result.messages;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Step-12.5 — `POST /admin/sessions/:id/purge-sandbox`.
|
|
700
|
+
*
|
|
701
|
+
* Removes the per-session sandbox folder from disk + clears
|
|
702
|
+
* `session.sandbox_dir`. Idempotent: 204 on success regardless of
|
|
703
|
+
* whether the session ever had a sandbox. Rejects with
|
|
704
|
+
* `409 turn_running` if a chat turn is live for the session.
|
|
705
|
+
*
|
|
706
|
+
* `super_admin` only — `admin` / `power_user` callers get 403.
|
|
707
|
+
*
|
|
708
|
+
* @param id - the ai-hub session id.
|
|
709
|
+
* @throws {AIHubRequestError} `turn_running`, `forbidden`, `not_found`.
|
|
710
|
+
*/
|
|
711
|
+
async purgeAdminSessionSandbox(id) {
|
|
712
|
+
await this.requestJson("POST", `/admin/sessions/${encodeURIComponent(id)}/purge-sandbox`);
|
|
713
|
+
}
|
|
714
|
+
/* ───── admin: settings ────────────────────────────────────────────── */
|
|
715
|
+
/**
|
|
716
|
+
* `GET /admin/settings` — deployment-wide configuration (super_admin only).
|
|
717
|
+
*
|
|
718
|
+
* The Letta API key is never returned in plaintext; the response carries
|
|
719
|
+
* `letta_api_key_set` + `letta_api_key_masked` so the UI can render
|
|
720
|
+
* "configured / not configured" affordances.
|
|
721
|
+
*/
|
|
722
|
+
async getAdminSettings() {
|
|
723
|
+
return this.requestJson("GET", "/admin/settings");
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* `PATCH /admin/settings` — update one or more deployment settings.
|
|
727
|
+
*
|
|
728
|
+
* The API key, when supplied, is encrypted under `AI_HUB_ENCRYPTION_KEY`
|
|
729
|
+
* server-side. Theme fields live as separate columns and follow the same
|
|
730
|
+
* precedence as the React props (server > prop > built-in default).
|
|
731
|
+
*
|
|
732
|
+
* @param body - the patch. Each field independent.
|
|
733
|
+
* @returns the refreshed settings view.
|
|
734
|
+
*/
|
|
735
|
+
async patchAdminSettings(body) {
|
|
736
|
+
const wire = {};
|
|
737
|
+
if (body.lettaBaseUrl !== undefined)
|
|
738
|
+
wire.letta_base_url = body.lettaBaseUrl;
|
|
739
|
+
if (body.lettaAdeUrl !== undefined)
|
|
740
|
+
wire.letta_ade_url = body.lettaAdeUrl;
|
|
741
|
+
if (body.lettaApiKey !== undefined)
|
|
742
|
+
wire.letta_api_key = body.lettaApiKey;
|
|
743
|
+
if (body.theme !== undefined) {
|
|
744
|
+
const themeWire = {};
|
|
745
|
+
if (body.theme.default !== undefined)
|
|
746
|
+
themeWire.default = body.theme.default;
|
|
747
|
+
if (body.theme.switcherEnabled !== undefined) {
|
|
748
|
+
themeWire.switcher_enabled = body.theme.switcherEnabled;
|
|
749
|
+
}
|
|
750
|
+
wire.theme = themeWire;
|
|
751
|
+
}
|
|
752
|
+
return this.requestJson("PATCH", "/admin/settings", {
|
|
753
|
+
body: wire,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* `GET /admin/settings/feature-toggles` — the §8.4 toggle blob the
|
|
758
|
+
* super-admin sees. Same payload the `getFeatureToggles` (unauthenticated)
|
|
759
|
+
* endpoint returns, but gated and intended for the settings editor.
|
|
760
|
+
*/
|
|
761
|
+
async getAdminFeatureToggles() {
|
|
762
|
+
const result = await this.requestJson("GET", "/admin/settings/feature-toggles");
|
|
763
|
+
return result.feature_toggles ?? {};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* `PATCH /admin/settings/feature-toggles` — merge-patch the §8.4 toggle
|
|
767
|
+
* blob. Unknown keys are rejected with 400; the server's strict schema is
|
|
768
|
+
* the source of truth for which toggles are accepted.
|
|
769
|
+
*
|
|
770
|
+
* @param toggles - the partial — only the keys you want to write.
|
|
771
|
+
*/
|
|
772
|
+
async patchAdminFeatureToggles(toggles) {
|
|
773
|
+
const result = await this.requestJson("PATCH", "/admin/settings/feature-toggles", { body: toggles });
|
|
774
|
+
return result.feature_toggles;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* `POST /admin/settings/rotate-signing-key` — replace the JWT signing key
|
|
778
|
+
* in place. **Every outstanding JWT is invalidated immediately**, including
|
|
779
|
+
* the caller's own. The next protected request from the UI will 401 and
|
|
780
|
+
* the provider routes back to login (§6.7).
|
|
781
|
+
*
|
|
782
|
+
* Returns nothing — the new key never leaves the server.
|
|
783
|
+
*/
|
|
784
|
+
async rotateAdminSigningKey() {
|
|
785
|
+
await this.requestJson("POST", "/admin/settings/rotate-signing-key", {
|
|
786
|
+
body: {},
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* `POST /admin/settings/rotate-metrics-token` — rotate the `/metrics`
|
|
791
|
+
* bearer token. The new token is returned **exactly once** in
|
|
792
|
+
* `{ token }`; subsequent `getAdminSettings` calls do NOT echo it back.
|
|
793
|
+
* The admin must copy it into their Prometheus / Alloy config before
|
|
794
|
+
* dismissing the reveal modal (§6.10).
|
|
795
|
+
*
|
|
796
|
+
* Does NOT invalidate the caller's JWT — only the metrics-scraper bearer
|
|
797
|
+
* changes.
|
|
798
|
+
*/
|
|
799
|
+
async rotateAdminMetricsToken() {
|
|
800
|
+
return this.requestJson("POST", "/admin/settings/rotate-metrics-token", { body: {} });
|
|
801
|
+
}
|
|
802
|
+
/* ───── internal request plumbing ─────────────────────────────────── */
|
|
803
|
+
/**
|
|
804
|
+
* Sends a JSON request and parses the JSON response. Centralizes auth
|
|
805
|
+
* header injection, 401 handling, and error normalization.
|
|
806
|
+
*
|
|
807
|
+
* @param method - HTTP method.
|
|
808
|
+
* @param path - path under `baseUrl`.
|
|
809
|
+
* @param opts - optional body and unauthenticated-mode flag.
|
|
810
|
+
* @param opts.body - JSON body to send (omit for GET / DELETE).
|
|
811
|
+
* @param opts.authenticated - set to `false` to skip the bearer header
|
|
812
|
+
* (login is the only such caller). Defaults to true.
|
|
813
|
+
* @returns the parsed response.
|
|
814
|
+
*/
|
|
815
|
+
async requestJson(method, path, opts = {}) {
|
|
816
|
+
const headers = { Accept: "application/json" };
|
|
817
|
+
const authenticated = opts.authenticated !== false;
|
|
818
|
+
if (authenticated) {
|
|
819
|
+
const token = this.tokenStore.get();
|
|
820
|
+
if (token)
|
|
821
|
+
headers.Authorization = `Bearer ${token}`;
|
|
822
|
+
}
|
|
823
|
+
let body;
|
|
824
|
+
if (opts.body !== undefined) {
|
|
825
|
+
headers["Content-Type"] = "application/json";
|
|
826
|
+
body = JSON.stringify(opts.body);
|
|
827
|
+
}
|
|
828
|
+
let response;
|
|
829
|
+
try {
|
|
830
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
831
|
+
method,
|
|
832
|
+
headers,
|
|
833
|
+
...(body !== undefined ? { body } : {}),
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
throw new AIHubRequestError("network", err instanceof Error ? err.message : "Network request failed.");
|
|
838
|
+
}
|
|
839
|
+
if (response.status === 401) {
|
|
840
|
+
this.handleUnauthorized();
|
|
841
|
+
}
|
|
842
|
+
if (!response.ok) {
|
|
843
|
+
throw await this.errorFromResponse(response);
|
|
844
|
+
}
|
|
845
|
+
// 204 / 205 / empty body
|
|
846
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
847
|
+
if (response.status === 204 || !ct.includes("application/json")) {
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
return (await response.json());
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Shared backbone for `streamChat` and `getTurnEvents`. Yields events
|
|
854
|
+
* from an SSE response; throws `AIHubRequestError` on 4xx/5xx before the
|
|
855
|
+
* stream opens.
|
|
856
|
+
*
|
|
857
|
+
* @param method - HTTP method.
|
|
858
|
+
* @param path - path under `baseUrl`.
|
|
859
|
+
* @param body - optional JSON body (POST /chat only).
|
|
860
|
+
* @param opts - abort signal.
|
|
861
|
+
* @yields each ai-hub event from the SSE stream until `stream_end`.
|
|
862
|
+
*/
|
|
863
|
+
async *streamRequest(method, path, body, opts) {
|
|
864
|
+
const headers = { Accept: "text/event-stream" };
|
|
865
|
+
const token = this.tokenStore.get();
|
|
866
|
+
if (token)
|
|
867
|
+
headers.Authorization = `Bearer ${token}`;
|
|
868
|
+
let bodyInit;
|
|
869
|
+
if (body !== undefined) {
|
|
870
|
+
headers["Content-Type"] = "application/json";
|
|
871
|
+
bodyInit = JSON.stringify(body);
|
|
872
|
+
}
|
|
873
|
+
let response;
|
|
874
|
+
try {
|
|
875
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
876
|
+
method,
|
|
877
|
+
headers,
|
|
878
|
+
...(bodyInit !== undefined ? { body: bodyInit } : {}),
|
|
879
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
throw new AIHubRequestError("network", err instanceof Error ? err.message : "SSE request failed.");
|
|
884
|
+
}
|
|
885
|
+
if (response.status === 401)
|
|
886
|
+
this.handleUnauthorized();
|
|
887
|
+
if (!response.ok)
|
|
888
|
+
throw await this.errorFromResponse(response);
|
|
889
|
+
if (!response.body) {
|
|
890
|
+
throw new AIHubRequestError("internal", "SSE response had no body.");
|
|
891
|
+
}
|
|
892
|
+
for await (const event of parseSseStream(response.body)) {
|
|
893
|
+
yield event;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Clears the token and fires the unauthorized callback (at most once per
|
|
898
|
+
* 401 storm — the flag resets on next `setToken`).
|
|
899
|
+
*/
|
|
900
|
+
handleUnauthorized() {
|
|
901
|
+
this.clearToken();
|
|
902
|
+
if (this.unauthorizedFired)
|
|
903
|
+
return;
|
|
904
|
+
this.unauthorizedFired = true;
|
|
905
|
+
this.onUnauthorized?.();
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Builds an `AIHubRequestError` from a non-ok server response. Reads the
|
|
909
|
+
* server's `{ error: { code, message, request_id } }` envelope when
|
|
910
|
+
* present; falls back to status-code-derived defaults otherwise.
|
|
911
|
+
*
|
|
912
|
+
* @param response - the fetch response (status >= 400).
|
|
913
|
+
* @returns the normalized error.
|
|
914
|
+
*/
|
|
915
|
+
async errorFromResponse(response) {
|
|
916
|
+
let envelope = {};
|
|
917
|
+
try {
|
|
918
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
919
|
+
if (ct.includes("application/json")) {
|
|
920
|
+
envelope = (await response.json());
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
/* malformed body — fall through to defaults */
|
|
925
|
+
}
|
|
926
|
+
const errorBlock = envelope.error;
|
|
927
|
+
const wireCode = errorBlock?.code;
|
|
928
|
+
const message = errorBlock?.message ?? `Request failed with status ${response.status}.`;
|
|
929
|
+
const code = wireCode ?? defaultCodeForStatus(response.status);
|
|
930
|
+
return new AIHubRequestError(code, message, response.status, errorBlock?.request_id);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Maps an HTTP status to a default `AIHubError.code` when the server didn't
|
|
935
|
+
* include one in the response envelope.
|
|
936
|
+
*/
|
|
937
|
+
function defaultCodeForStatus(status) {
|
|
938
|
+
if (status === 401)
|
|
939
|
+
return "unauthorized";
|
|
940
|
+
if (status === 403)
|
|
941
|
+
return "forbidden";
|
|
942
|
+
if (status === 404)
|
|
943
|
+
return "not_found";
|
|
944
|
+
if (status >= 400 && status < 500)
|
|
945
|
+
return "validation";
|
|
946
|
+
if (status >= 500)
|
|
947
|
+
return "internal";
|
|
948
|
+
return "internal";
|
|
949
|
+
}
|
|
950
|
+
//# sourceMappingURL=AIHubClient.js.map
|