Compare commits

..
Author SHA1 Message Date
Antje Worring 61cb192134 fix(api): restore deleted Message/Conversation/Preset/Prompt models (runtime MODULE_NOT_FOUND)
The upstream LibreChat consolidation (#11830) deleted api/models/{Message,
Conversation,Preset,Prompt}.js and moved their logic into @librechat/data-schemas'
createMethods. A later conflict-resolution merge (73d4b423a) reverted the
data-schemas createMethods composition back to the pre-consolidation form (so it
no longer provides message/conversation/preset/prompt methods) but did NOT restore
the deleted api/models/*.js files. The result is a half-applied consolidation:

  - api/models/index.js still requires ./Message, ./Conversation, ./Preset
  - api/server/middleware/error.js, .../abortRun.js, .../fork.js, prompt access
    guards, etc. still require('~/models/Message' | '~/models/Conversation' |
    '~/models/Prompt')
  - but those files no longer exist -> backend crashes at startup with
    MODULE_NOT_FOUND (api/models/index.js:13 -> ./Message), crashlooping the pod.

Frontend build + jest passed because they never exercised this require chain.

Fix: restore the four model files from their last good pre-#11830 revision
(214252d2e, on this branch's own ancestry, so namespaces match HEAD:
@librechat/data-schemas, librechat-data-provider, ~/db/models), updating only the
@librechat/api import to @hanzochat/api to match the forked package (#36).

Verified locally with module-alias registered as the server does:
  require('./models') loads (138 exports, all message/convo/preset present);
  the full crash chain models -> banViolation -> logViolation -> cache ->
  db/indexSync -> server/middleware/error resolves with no MODULE_NOT_FOUND;
  @hanzochat/api still exports handleError/sendEvent/createTempChatExpirationDate/
  escapeRegExp/sanitizeMessageForTransmit.
2026-06-20 01:08:47 -07:00
5 changed files with 374 additions and 291 deletions
-2
View File
@@ -136,7 +136,6 @@
"typescript-eslint": "^8.24.0"
},
"overrides": {
"@ariakit/react": "0.4.29",
"@anthropic-ai/sdk": "0.73.0",
"@librechat/agents": {
"@langchain/anthropic": {
@@ -193,7 +192,6 @@
"unrs-resolver"
],
"overrides": {
"@ariakit/react": "0.4.29",
"zod-to-json-schema": "3.24.6",
"@modelcontextprotocol/sdk": "1.22.0"
}
+278 -197
View File
@@ -1,9 +1,13 @@
/**
* Hanzo Chat API Endpoints (LibreChat-native backend under /api/*).
* Hanzo Cloud Gateway API Endpoints
*
* Single source of truth for all REST URLs. The SPA is served by, and talks to,
* its own origin (e.g. https://hanzo.chat): BASE_URL is derived from the <base>
* element / relative path, with an optional explicit override via setApiBaseUrl().
* Single source of truth for all API URLs.
* The cloud gateway at api.hanzo.ai provides:
* - REST endpoints for CRUD (chats, messages, files, auth)
* - OpenAI/Anthropic-compatible LLM endpoints
* - ZAP WebSocket for streaming & MCP tools
*
* No backward compat, no adapter layers. One way to do everything.
*/
import type { AssistantsEndpoint } from './schemas';
import * as q from './types/queries';
@@ -13,18 +17,24 @@ import { ResourceType } from './accessPermissions';
// Base URL resolution
// ---------------------------------------------------------------------------
/** Explicit API base URL (set via setApiBaseUrl(), e.g. for SSR/tests) */
/** Explicit API base URL (set via VITE_HANZO_API_URL env var or setApiBaseUrl()) */
let _explicitBaseUrl: string | undefined;
/**
* Resolve the API base URL.
* Priority: explicit override > <base> element > relative ''
* Priority: explicit > env var > <base> element > relative ''
*/
function resolveBaseUrl(): string {
if (_explicitBaseUrl !== undefined) {
return _explicitBaseUrl;
}
// Check Vite env var (available at build time)
if (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_HANZO_API_URL) {
return ((import.meta as any).env.VITE_HANZO_API_URL as string).replace(/\/$/, '');
}
// Fallback: <base> element or relative path
if (
typeof process === 'undefined' ||
(process as typeof process & { browser?: boolean }).browser === true
@@ -39,7 +49,7 @@ function resolveBaseUrl(): string {
let BASE_URL = resolveBaseUrl();
/** Runtime override for API base URL (SSR/tests) */
/** Runtime override for API base URL */
export function setApiBaseUrl(url: string) {
_explicitBaseUrl = url.replace(/\/$/, '');
BASE_URL = _explicitBaseUrl;
@@ -47,12 +57,20 @@ export function setApiBaseUrl(url: string) {
export const apiBaseUrl = () => BASE_URL;
/** ZAP WebSocket URL (derived from API base; retained for compat). */
/** ZAP WebSocket URL (derived from API base, or explicit) */
export function zapUrl(): string {
const base = BASE_URL || '';
if (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_HANZO_ZAP_URL) {
return (import.meta as any).env.VITE_HANZO_ZAP_URL as string;
}
// Convert http(s):// to ws(s)://
const base = BASE_URL || 'https://api.hanzo.ai';
return base.replace(/^http/, 'ws') + '/zap';
}
// Testing this buildQuery function
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const buildQuery = (params: Record<string, unknown>): string => {
const query = Object.entries(params)
.filter(([, value]) => {
@@ -71,36 +89,125 @@ const buildQuery = (params: Record<string, unknown>): string => {
return query ? `?${query}` : '';
};
export const health = () => `${BASE_URL}/health`;
export const user = () => `${BASE_URL}/api/user`;
// ---------------------------------------------------------------------------
// Health & System
// ---------------------------------------------------------------------------
export const balance = () => `${BASE_URL}/api/balance`;
export const health = () => `${BASE_URL}/v1/health`;
export const systemInfo = () => `${BASE_URL}/v1/get-system-info`;
export const versionInfo = () => `${BASE_URL}/v1/get-version-info`;
export const userPlugins = () => `${BASE_URL}/api/user/plugins`;
// ---------------------------------------------------------------------------
// Auth (Hanzo IAM via Cloud Gateway)
// ---------------------------------------------------------------------------
export const deleteUser = () => `${BASE_URL}/api/user/delete`;
/** Sign in via hanzo.id OAuth code exchange */
export const signin = (code: string, state: string) =>
`${BASE_URL}/v1/signin?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
const messagesRoot = `${BASE_URL}/api/messages`;
/** Sign out */
export const signout = () => `${BASE_URL}/v1/signout`;
/** Get current account (session-based) */
export const getAccount = () => `${BASE_URL}/v1/get-account`;
// Legacy aliases for frontend compatibility
export const login = () => `${BASE_URL}/v1/signin`;
export const logout = () => `${BASE_URL}/v1/signout`;
export const register = () => `${BASE_URL}/v1/signin`; // No separate registration — IAM handles it
export const refreshToken = (retry?: boolean) => `${BASE_URL}/v1/get-account`;
export const user = () => `${BASE_URL}/v1/get-account`;
// Auth page URLs (client-side navigation)
export const loginPage = () => `${BASE_URL}/login`;
export const registerPage = () => `${BASE_URL}/register`;
// Social login (handled by IAM redirect, not gateway)
export const loginFacebook = () => `${BASE_URL}/v1/signin`;
export const loginGoogle = () => `${BASE_URL}/v1/signin`;
// Email verification (handled by IAM)
export const verifyEmail = () => `${BASE_URL}/v1/get-account`;
export const resendVerificationEmail = () => `${BASE_URL}/v1/get-account`;
export const requestPasswordReset = () => `${BASE_URL}/v1/get-account`;
export const resetPassword = () => `${BASE_URL}/v1/get-account`;
// 2FA (handled by IAM directly)
export const enableTwoFactor = () => `${BASE_URL}/v1/get-account`;
export const verifyTwoFactor = () => `${BASE_URL}/v1/get-account`;
export const confirmTwoFactor = () => `${BASE_URL}/v1/get-account`;
export const disableTwoFactor = () => `${BASE_URL}/v1/get-account`;
export const regenerateBackupCodes = () => `${BASE_URL}/v1/get-account`;
export const verifyTwoFactorTemp = () => `${BASE_URL}/v1/get-account`;
// ---------------------------------------------------------------------------
// User & Balance
// ---------------------------------------------------------------------------
export const balance = () => `${BASE_URL}/v1/get-usages`;
export const userPlugins = () => `${BASE_URL}/v1/get-account`;
export const deleteUser = () => `${BASE_URL}/v1/get-account`;
// ---------------------------------------------------------------------------
// Conversations (Chats)
// ---------------------------------------------------------------------------
export const conversationsRoot = `${BASE_URL}/v1`;
export const conversations = (params: q.ConversationListParams) => {
return `${BASE_URL}/v1/get-chats${buildQuery(params)}`;
};
export const conversationById = (id: string) =>
`${BASE_URL}/v1/get-chat?id=${encodeURIComponent(id)}`;
export const genTitle = (conversationId: string) =>
`${BASE_URL}/v1/get-answer?provider=&question=${encodeURIComponent('Generate a short title for this conversation')}`;
export const updateConversation = () => `${BASE_URL}/v1/update-chat`;
export const archiveConversation = () => `${BASE_URL}/v1/update-chat`;
export const deleteConversation = () => `${BASE_URL}/v1/delete-chat`;
export const deleteAllConversation = () => `${BASE_URL}/v1/delete-chat`;
export const importConversation = () => `${BASE_URL}/v1/add-chat`;
export const forkConversation = () => `${BASE_URL}/v1/add-chat`;
export const duplicateConversation = () => `${BASE_URL}/v1/add-chat`;
// ---------------------------------------------------------------------------
// Messages
// ---------------------------------------------------------------------------
const messagesRoot = `${BASE_URL}/v1`;
export const messages = (params: q.MessagesListParams) => {
const { conversationId, messageId, ...rest } = params;
if (conversationId && messageId) {
return `${messagesRoot}/${conversationId}/${messageId}`;
return `${BASE_URL}/v1/get-message?id=${encodeURIComponent(messageId)}`;
}
if (conversationId) {
return `${messagesRoot}/${conversationId}`;
return `${BASE_URL}/v1/get-messages?chat=${encodeURIComponent(conversationId)}`;
}
return `${messagesRoot}${buildQuery(rest)}`;
return `${BASE_URL}/v1/get-messages${buildQuery(rest)}`;
};
export const messagesArtifacts = (messageId: string) => `${messagesRoot}/artifact/${messageId}`;
export const messagesArtifacts = (messageId: string) =>
`${BASE_URL}/v1/get-message?id=${encodeURIComponent(messageId)}`;
export const messagesBranch = () => `${messagesRoot}/branch`;
export const messagesBranch = () => `${BASE_URL}/v1/add-message`;
const shareRoot = `${BASE_URL}/api/share`;
// ---------------------------------------------------------------------------
// Shares
// ---------------------------------------------------------------------------
const shareRoot = `${BASE_URL}/v1/share`;
export const shareMessages = (shareId: string) => `${shareRoot}/${shareId}`;
export const getSharedLink = (conversationId: string) => `${shareRoot}/link/${conversationId}`;
export const getSharedLinks = (
@@ -117,113 +224,91 @@ export const getSharedLinks = (
export const createSharedLink = (conversationId: string) => `${shareRoot}/${conversationId}`;
export const updateSharedLink = (shareId: string) => `${shareRoot}/${shareId}`;
const keysEndpoint = `${BASE_URL}/api/keys`;
// ---------------------------------------------------------------------------
// API Keys (pk- / sk- via LLM gateway)
// ---------------------------------------------------------------------------
const keysEndpoint = `${BASE_URL}/v1/keys`;
export const keys = () => keysEndpoint;
export const userKeyQuery = (name: string) => `${keysEndpoint}?name=${name}`;
export const revokeUserKey = (name: string) => `${keysEndpoint}/${name}`;
export const revokeAllUserKeys = () => `${keysEndpoint}?all=true`;
const apiKeysEndpoint = `${BASE_URL}/api/api-keys`;
const apiKeysEndpoint = `${BASE_URL}/v1/api-keys`;
export const apiKeys = () => apiKeysEndpoint;
export const apiKeyById = (id: string) => `${apiKeysEndpoint}/${id}`;
export const conversationsRoot = `${BASE_URL}/api/convos`;
export const conversations = (params: q.ConversationListParams) => {
return `${conversationsRoot}${buildQuery(params)}`;
};
export const conversationById = (id: string) => `${conversationsRoot}/${id}`;
export const genTitle = (conversationId: string) =>
`${conversationsRoot}/gen_title/${encodeURIComponent(conversationId)}`;
export const updateConversation = () => `${conversationsRoot}/update`;
export const archiveConversation = () => `${conversationsRoot}/archive`;
export const deleteConversation = () => `${conversationsRoot}`;
export const deleteAllConversation = () => `${conversationsRoot}/all`;
export const importConversation = () => `${conversationsRoot}/import`;
export const forkConversation = () => `${conversationsRoot}/fork`;
export const duplicateConversation = () => `${conversationsRoot}/duplicate`;
// ---------------------------------------------------------------------------
// Search
// ---------------------------------------------------------------------------
export const search = (q: string, cursor?: string | null) =>
`${BASE_URL}/api/search?q=${q}${cursor ? `&cursor=${cursor}` : ''}`;
`${BASE_URL}/v1/get-global-chats?field=messages&value=${encodeURIComponent(q)}${cursor ? `&cursor=${cursor}` : ''}`;
export const searchEnabled = () => `${BASE_URL}/api/search/enable`;
export const searchEnabled = () => `${BASE_URL}/v1/health`;
export const presets = () => `${BASE_URL}/api/presets`;
// ---------------------------------------------------------------------------
// Presets & Config
// ---------------------------------------------------------------------------
export const deletePreset = () => `${BASE_URL}/api/presets/delete`;
export const presets = () => `${BASE_URL}/v1/presets`;
export const deletePreset = () => `${BASE_URL}/v1/presets/delete`;
export const plugins = () => `${BASE_URL}/v1/get-providers`;
export const config = () => `${BASE_URL}/v1/get-account`;
export const aiEndpoints = () => `${BASE_URL}/v1/get-providers`;
export const aiEndpoints = () => `${BASE_URL}/api/endpoints`;
// ---------------------------------------------------------------------------
// Models (OpenAI-compatible, pk- key accessible)
// ---------------------------------------------------------------------------
export const models = () => `${BASE_URL}/api/models`;
export const models = () => `${BASE_URL}/v1/models`;
export const tokenizer = () => `${BASE_URL}/api/tokenizer`;
export const tokenizer = () => `${BASE_URL}/v1/tokenizer`;
export const login = () => `${BASE_URL}/api/auth/login`;
// ---------------------------------------------------------------------------
// LLM Completions (OpenAI + Anthropic compatible)
// ---------------------------------------------------------------------------
export const logout = () => `${BASE_URL}/api/auth/logout`;
export const chatCompletions = () => `${BASE_URL}/v1/chat/completions`;
export const anthropicMessages = () => `${BASE_URL}/v1/messages`;
export const register = () => `${BASE_URL}/api/auth/register`;
// ---------------------------------------------------------------------------
// Providers
// ---------------------------------------------------------------------------
export const loginFacebook = () => `${BASE_URL}/api/auth/facebook`;
export const providers = () => `${BASE_URL}/v1/get-providers`;
export const provider = (id: string) => `${BASE_URL}/v1/get-provider?id=${encodeURIComponent(id)}`;
export const loginGoogle = () => `${BASE_URL}/api/auth/google`;
// ---------------------------------------------------------------------------
// Files
// ---------------------------------------------------------------------------
export const refreshToken = (retry?: boolean) =>
`${BASE_URL}/api/auth/refresh${retry === true ? '?retry=true' : ''}`;
export const files = () => `${BASE_URL}/v1/get-files`;
export const fileUpload = () => `${BASE_URL}/v1/upload-file`;
export const fileDelete = () => `${BASE_URL}/v1/delete-file`;
export const fileDownload = (userId: string, fileId: string) =>
`${BASE_URL}/v1/get-file?id=${encodeURIComponent(fileId)}`;
export const fileConfig = () => `${BASE_URL}/v1/get-files`;
export const agentFiles = (agentId: string) =>
`${BASE_URL}/v1/get-files?agent=${encodeURIComponent(agentId)}`;
export const requestPasswordReset = () => `${BASE_URL}/api/auth/requestPasswordReset`;
export const images = () => `${BASE_URL}/v1/get-files`;
export const avatar = () => `${BASE_URL}/v1/upload-file`;
export const resetPassword = () => `${BASE_URL}/api/auth/resetPassword`;
// ---------------------------------------------------------------------------
// Speech (TTS / STT)
// ---------------------------------------------------------------------------
export const verifyEmail = () => `${BASE_URL}/api/user/verify`;
export const speech = () => `${BASE_URL}/v1`;
export const speechToText = () => `${BASE_URL}/v1/process-speech-to-text`;
export const textToSpeech = () => `${BASE_URL}/v1/generate-text-to-speech-audio`;
export const textToSpeechManual = () => `${BASE_URL}/v1/generate-text-to-speech-audio`;
export const textToSpeechVoices = () => `${BASE_URL}/v1/generate-text-to-speech-audio`;
export const getCustomConfigSpeech = () => `${BASE_URL}/v1/get-providers`;
// Auth page URLs (for client-side navigation and redirects)
export const loginPage = () => `${BASE_URL}/login`;
export const registerPage = () => `${BASE_URL}/register`;
export const resendVerificationEmail = () => `${BASE_URL}/api/user/verify/resend`;
export const plugins = () => `${BASE_URL}/api/plugins`;
export const mcpReinitialize = (serverName: string) =>
`${BASE_URL}/api/mcp/${serverName}/reinitialize`;
export const mcpConnectionStatus = () => `${BASE_URL}/api/mcp/connection/status`;
export const mcpServerConnectionStatus = (serverName: string) =>
`${BASE_URL}/api/mcp/connection/status/${serverName}`;
export const mcpAuthValues = (serverName: string) => {
return `${BASE_URL}/api/mcp/${serverName}/auth-values`;
};
export const cancelMCPOAuth = (serverName: string) => {
return `${BASE_URL}/api/mcp/oauth/cancel/${serverName}`;
};
export const mcpOAuthBind = (serverName: string) => `${BASE_URL}/api/mcp/${serverName}/oauth/bind`;
export const actionOAuthBind = (actionId: string) =>
`${BASE_URL}/api/actions/${actionId}/oauth/bind`;
export const config = () => `${BASE_URL}/api/config`;
export const prompts = () => `${BASE_URL}/api/prompts`;
export const addPromptToGroup = (groupId: string) =>
`${BASE_URL}/api/prompts/groups/${groupId}/prompts`;
// ---------------------------------------------------------------------------
// Assistants & Agents
// ---------------------------------------------------------------------------
export const assistants = ({
path = '',
@@ -238,82 +323,72 @@ export const assistants = ({
version: number | string;
isAvatar?: boolean;
}) => {
let url = isAvatar === true ? `${images()}/assistants` : `${BASE_URL}/api/assistants/v${version}`;
let url = isAvatar === true ? `${images()}/assistants` : `${BASE_URL}/v1/assistants/v${version}`;
if (path && path !== '') {
url += `/${path}`;
}
if (endpoint) {
options = {
...(options ?? {}),
endpoint,
};
options = { ...(options ?? {}), endpoint };
}
if (options && Object.keys(options).length > 0) {
const queryParams = new URLSearchParams(options as Record<string, string>).toString();
url += `?${queryParams}`;
}
return url;
};
export const agents = ({ path = '', options }: { path?: string; options?: object }) => {
let url = `${BASE_URL}/api/agents`;
let url = `${BASE_URL}/v1/agents`;
if (path && path !== '') {
url += `/${path}`;
}
if (options && Object.keys(options).length > 0) {
const queryParams = new URLSearchParams(options as Record<string, string>).toString();
url += `?${queryParams}`;
}
return url;
};
export const activeJobs = () => `${BASE_URL}/api/agents/chat/active`;
export const mcp = {
tools: `${BASE_URL}/api/mcp/tools`,
servers: `${BASE_URL}/api/mcp/servers`,
};
export const mcpServer = (serverName: string) => `${BASE_URL}/api/mcp/servers/${serverName}`;
export const activeJobs = () => `${BASE_URL}/v1/agents/chat/active`;
export const revertAgentVersion = (agent_id: string) => `${agents({ path: `${agent_id}/revert` })}`;
export const files = () => `${BASE_URL}/api/files`;
export const fileUpload = () => `${BASE_URL}/api/files`;
export const fileDelete = () => `${BASE_URL}/api/files`;
export const fileDownload = (userId: string, fileId: string) =>
`${BASE_URL}/api/files/download/${userId}/${fileId}`;
export const fileConfig = () => `${BASE_URL}/api/files/config`;
export const agentFiles = (agentId: string) => `${BASE_URL}/api/files/agent/${agentId}`;
// ---------------------------------------------------------------------------
// MCP (Model Context Protocol) — via REST or ZAP
// ---------------------------------------------------------------------------
export const images = () => `${files()}/images`;
export const mcp = {
tools: `${BASE_URL}/v1/mcp/tools`,
servers: `${BASE_URL}/v1/mcp/servers`,
};
export const avatar = () => `${images()}/avatar`;
export const mcpServer = (serverName: string) => `${BASE_URL}/v1/mcp/servers/${serverName}`;
export const mcpReinitialize = (serverName: string) =>
`${BASE_URL}/v1/mcp/${serverName}/reinitialize`;
export const mcpConnectionStatus = () => `${BASE_URL}/v1/mcp/connection/status`;
export const mcpServerConnectionStatus = (serverName: string) =>
`${BASE_URL}/v1/mcp/connection/status/${serverName}`;
export const mcpAuthValues = (serverName: string) =>
`${BASE_URL}/v1/mcp/${serverName}/auth-values`;
export const cancelMCPOAuth = (serverName: string) =>
`${BASE_URL}/v1/mcp/oauth/cancel/${serverName}`;
export const mcpOAuthBind = (serverName: string) => `${BASE_URL}/v1/mcp/${serverName}/oauth/bind`;
export const actionOAuthBind = (actionId: string) =>
`${BASE_URL}/v1/actions/${actionId}/oauth/bind`;
export const speech = () => `${files()}/speech`;
// MCP tool refresh (cloud gateway native)
export const refreshMcpTools = () => `${BASE_URL}/v1/refresh-mcp-tools`;
export const speechToText = () => `${speech()}/stt`;
export const textToSpeech = () => `${speech()}/tts`;
export const textToSpeechManual = () => `${textToSpeech()}/manual`;
export const textToSpeechVoices = () => `${textToSpeech()}/voices`;
export const getCustomConfigSpeech = () => `${speech()}/config/get`;
// ---------------------------------------------------------------------------
// Prompts
// ---------------------------------------------------------------------------
export const prompts = () => `${BASE_URL}/v1/prompts`;
export const addPromptToGroup = (groupId: string) =>
`${BASE_URL}/v1/prompts/groups/${groupId}/prompts`;
export const getPromptGroup = (_id: string) => `${prompts()}/groups/${_id}`;
export const getPromptGroupsWithFilters = (filter: object) => {
let url = `${prompts()}/groups`;
// Filter out undefined/null values
const cleanedFilter = Object.entries(filter).reduce(
(acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
@@ -323,14 +398,12 @@ export const getPromptGroupsWithFilters = (filter: object) => {
},
{} as Record<string, string>,
);
if (Object.keys(cleanedFilter).length > 0) {
const queryParams = new URLSearchParams(cleanedFilter).toString();
url += `?${queryParams}`;
}
return url;
};
export const getPromptsWithFilters = (filter: object) => {
let url = prompts();
if (Object.keys(filter).length > 0) {
@@ -339,33 +412,25 @@ export const getPromptsWithFilters = (filter: object) => {
}
return url;
};
export const getPrompt = (_id: string) => `${prompts()}/${_id}`;
export const getRandomPrompts = (limit: number, skip: number) =>
`${prompts()}/random?limit=${limit}&skip=${skip}`;
export const postPrompt = prompts;
export const updatePromptGroup = getPromptGroup;
export const updatePromptLabels = (_id: string) => `${getPrompt(_id)}/labels`;
export const updatePromptTag = (_id: string) => `${getPrompt(_id)}/tags/production`;
export const deletePromptGroup = getPromptGroup;
export const deletePrompt = ({ _id, groupId }: { _id: string; groupId: string }) => {
return `${prompts()}/${_id}?groupId=${groupId}`;
};
export const getCategories = () => `${BASE_URL}/api/categories`;
export const deletePrompt = ({ _id, groupId }: { _id: string; groupId: string }) =>
`${prompts()}/${_id}?groupId=${groupId}`;
export const getCategories = () => `${BASE_URL}/v1/categories`;
export const getAllPromptGroups = () => `${prompts()}/all`;
/* Roles */
export const roles = () => `${BASE_URL}/api/roles`;
export const getRole = (roleName: string) => `${roles()}/${roleName.toLowerCase()}`;
// ---------------------------------------------------------------------------
// Roles & Permissions
// ---------------------------------------------------------------------------
export const roles = () => `${BASE_URL}/v1/get-permissions`;
export const getRole = (roleName: string) => `${BASE_URL}/v1/get-permission?id=${roleName.toLowerCase()}`;
export const updatePromptPermissions = (roleName: string) => `${getRole(roleName)}/prompts`;
export const updateMemoryPermissions = (roleName: string) => `${getRole(roleName)}/memories`;
export const updateAgentPermissions = (roleName: string) => `${getRole(roleName)}/agents`;
@@ -374,73 +439,89 @@ export const updatePeoplePickerPermissions = (roleName: string) =>
export const updateMCPServersPermissions = (roleName: string) => `${getRole(roleName)}/mcp-servers`;
export const updateRemoteAgentsPermissions = (roleName: string) =>
`${getRole(roleName)}/remote-agents`;
export const updateMarketplacePermissions = (roleName: string) =>
`${getRole(roleName)}/marketplace`;
/* Conversation Tags */
export const conversationTags = (tag?: string) =>
`${BASE_URL}/api/tags${tag != null && tag ? `/${encodeURIComponent(tag)}` : ''}`;
// ---------------------------------------------------------------------------
// Conversation Tags
// ---------------------------------------------------------------------------
export const conversationTags = (tag?: string) =>
`${BASE_URL}/v1/tags${tag != null && tag ? `/${encodeURIComponent(tag)}` : ''}`;
export const conversationTagsList = (pageNumber: string, sort?: string, order?: string) =>
`${conversationTags()}/list?pageNumber=${pageNumber}${sort ? `&sort=${sort}` : ''}${
order ? `&order=${order}` : ''
}`;
export const addTagToConversation = (conversationId: string) =>
`${conversationTags()}/convo/${conversationId}`;
export const userTerms = () => `${BASE_URL}/api/user/terms`;
export const acceptUserTerms = () => `${BASE_URL}/api/user/terms/accept`;
export const banner = () => `${BASE_URL}/api/banner`;
// ---------------------------------------------------------------------------
// Misc
// ---------------------------------------------------------------------------
export const userTerms = () => `${BASE_URL}/v1/user/terms`;
export const acceptUserTerms = () => `${BASE_URL}/v1/user/terms/accept`;
export const banner = () => `${BASE_URL}/v1/banner`;
// Message Feedback
export const feedback = (conversationId: string, messageId: string) =>
`${BASE_URL}/api/messages/${conversationId}/${messageId}/feedback`;
`${BASE_URL}/v1/update-message?id=${encodeURIComponent(messageId)}`;
// Two-Factor Endpoints
export const enableTwoFactor = () => `${BASE_URL}/api/auth/2fa/enable`;
export const verifyTwoFactor = () => `${BASE_URL}/api/auth/2fa/verify`;
export const confirmTwoFactor = () => `${BASE_URL}/api/auth/2fa/confirm`;
export const disableTwoFactor = () => `${BASE_URL}/api/auth/2fa/disable`;
export const regenerateBackupCodes = () => `${BASE_URL}/api/auth/2fa/backup/regenerate`;
export const verifyTwoFactorTemp = () => `${BASE_URL}/api/auth/2fa/verify-temp`;
/* Memories */
export const memories = () => `${BASE_URL}/api/memories`;
// Memories
export const memories = () => `${BASE_URL}/v1/memories`;
export const memory = (key: string) => `${memories()}/${encodeURIComponent(key)}`;
export const memoryPreferences = () => `${memories()}/preferences`;
// Permissions search
export const searchPrincipals = (params: q.PrincipalSearchParams) => {
const { q: query, limit, types } = params;
let url = `${BASE_URL}/api/permissions/search-principals?q=${encodeURIComponent(query)}`;
let url = `${BASE_URL}/v1/permissions/search-principals?q=${encodeURIComponent(query)}`;
if (limit !== undefined) {
url += `&limit=${limit}`;
}
if (types && types.length > 0) {
url += `&types=${types.join(',')}`;
}
return url;
};
export const getAccessRoles = (resourceType: ResourceType) =>
`${BASE_URL}/api/permissions/${resourceType}/roles`;
`${BASE_URL}/v1/permissions/${resourceType}/roles`;
export const getResourcePermissions = (resourceType: ResourceType, resourceId: string) =>
`${BASE_URL}/api/permissions/${resourceType}/${resourceId}`;
`${BASE_URL}/v1/permissions/${resourceType}/${resourceId}`;
export const updateResourcePermissions = (resourceType: ResourceType, resourceId: string) =>
`${BASE_URL}/api/permissions/${resourceType}/${resourceId}`;
`${BASE_URL}/v1/permissions/${resourceType}/${resourceId}`;
export const getEffectivePermissions = (resourceType: ResourceType, resourceId: string) =>
`${BASE_URL}/api/permissions/${resourceType}/${resourceId}/effective`;
`${BASE_URL}/v1/permissions/${resourceType}/${resourceId}/effective`;
export const getAllEffectivePermissions = (resourceType: ResourceType) =>
`${BASE_URL}/api/permissions/${resourceType}/effective/all`;
`${BASE_URL}/v1/permissions/${resourceType}/effective/all`;
// SharePoint Graph API Token
// SharePoint Graph API Token (IAM-managed)
export const graphToken = (scopes: string) =>
`${BASE_URL}/api/auth/graph-token?scopes=${encodeURIComponent(scopes)}`;
`${BASE_URL}/v1/auth/graph-token?scopes=${encodeURIComponent(scopes)}`;
// ---------------------------------------------------------------------------
// Knowledge Stores (RAG)
// ---------------------------------------------------------------------------
export const stores = () => `${BASE_URL}/v1/get-stores`;
export const store = (id: string) => `${BASE_URL}/v1/get-store?id=${encodeURIComponent(id)}`;
export const addStore = () => `${BASE_URL}/v1/add-store`;
export const updateStore = () => `${BASE_URL}/v1/update-store`;
export const deleteStore = () => `${BASE_URL}/v1/delete-store`;
export const refreshStoreVectors = () => `${BASE_URL}/v1/refresh-store-vectors`;
// ---------------------------------------------------------------------------
// Vectors (Embeddings)
// ---------------------------------------------------------------------------
export const vectors = () => `${BASE_URL}/v1/get-vectors`;
export const addVector = () => `${BASE_URL}/v1/add-vector`;
export const deleteVector = () => `${BASE_URL}/v1/delete-vector`;
// ---------------------------------------------------------------------------
// Workflows & Tasks
// ---------------------------------------------------------------------------
export const workflows = () => `${BASE_URL}/v1/get-workflows`;
export const tasks = () => `${BASE_URL}/v1/get-tasks`;
+3 -3
View File
@@ -1244,9 +1244,9 @@ export const initialModelsConfig: TModelsConfig = {
};
export const EndpointURLs = {
[EModelEndpoint.assistants]: `${apiBaseUrl()}/api/assistants/v2/chat`,
[EModelEndpoint.azureAssistants]: `${apiBaseUrl()}/api/assistants/v1/chat`,
[EModelEndpoint.agents]: `${apiBaseUrl()}/api/${EModelEndpoint.agents}/chat`,
[EModelEndpoint.assistants]: `${apiBaseUrl()}/v1/assistants/v2/chat`,
[EModelEndpoint.azureAssistants]: `${apiBaseUrl()}/v1/assistants/v1/chat`,
[EModelEndpoint.agents]: `${apiBaseUrl()}/v1/${EModelEndpoint.agents}/chat`,
} as const;
export const modularEndpoints = new Set<EModelEndpoint | string>([
+76 -22
View File
@@ -1,19 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* HTTP request layer for the Hanzo Chat (LibreChat-native) backend.
* HTTP request layer for Hanzo Cloud Gateway.
*
* - Talks to the same-origin `/api/*` REST surface.
* - Auth is JWT Bearer (set via setTokenHeader) plus the httpOnly `refreshToken`
* cookie; `withCredentials: true` ensures that cookie is sent so the silent
* refresh against `POST /api/auth/refresh` can mint a fresh access token.
* - On a 401 the interceptor refreshes once and replays the original request.
* - pk- key support is retained for unauthenticated access (model listing, etc.).
* Handles:
* - Response unwrapping: gateway returns { status: "ok", data: X } → X
* - Session-based auth (cookies) with fallback to Bearer token
* - Auto-retry on session expiry via /api/get-account
* - pk- key support for unauthenticated requests
*/
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import * as endpoints from './api-endpoints';
import { setTokenHeader } from './headers-helpers';
import type * as t from './types';
// ---------------------------------------------------------------------------
// Cloud Gateway response unwrapper
// ---------------------------------------------------------------------------
/**
* The cloud gateway wraps all responses:
* { status: "ok", data: <payload>, data2?: <count> }
* { status: "error", msg: "..." }
*
* This interceptor unwraps successful responses to return `data` directly,
* which is what the frontend components expect.
*/
axios.interceptors.response.use(
(response) => {
const body = response.data;
// If the response has the gateway wrapper format, unwrap it
if (body && typeof body === 'object' && 'status' in body) {
if (body.status === 'ok') {
// Preserve pagination info if present
if (body.data2 !== undefined) {
response.data = body.data;
(response as any)._totalCount = body.data2;
} else {
response.data = body.data;
}
} else if (body.status === 'error') {
// Convert gateway error to rejection
const error = new Error(body.msg || 'Unknown error');
(error as any).response = response;
return Promise.reject(error);
}
}
return response;
},
(error) => Promise.reject(error),
);
// ---------------------------------------------------------------------------
// Request methods
// ---------------------------------------------------------------------------
async function _get<T>(url: string, options?: AxiosRequestConfig): Promise<T> {
const response = await axios.get(url, { withCredentials: true, ...options });
return response.data;
@@ -83,8 +124,12 @@ async function _patch(url: string, data?: any) {
let isRefreshing = false;
let failedQueue: { resolve: (value?: any) => void; reject: (reason?: any) => void }[] = [];
const refreshToken = (retry?: boolean): Promise<t.TRefreshTokenResponse | undefined> =>
_post(endpoints.refreshToken(retry));
/**
* Refresh session by calling get-account.
* Cloud gateway uses server-side sessions (cookies), so this just
* validates the session is still alive and returns fresh user data.
*/
const refreshSession = (): Promise<any> => _get(endpoints.getAccount());
const dispatchTokenUpdatedEvent = (token: string) => {
setTokenHeader(token);
@@ -102,7 +147,7 @@ const processQueue = (error: AxiosError | null, token: string | null = null) =>
failedQueue = [];
};
// Auto-retry on 401 (access token expired): refresh once, then replay.
// Auto-retry on 401 (session expired)
if (typeof window !== 'undefined') {
axios.interceptors.response.use(
(response) => response,
@@ -112,11 +157,14 @@ if (typeof window !== 'undefined') {
return Promise.reject(error);
}
// Don't retry auth endpoints that legitimately 401.
if (originalRequest.url?.includes('/api/auth/2fa') === true) {
// Don't retry auth endpoints
if (originalRequest.url?.includes('/v1/signin') === true) {
return Promise.reject(error);
}
if (originalRequest.url?.includes('/api/auth/logout') === true) {
if (originalRequest.url?.includes('/v1/signout') === true) {
return Promise.reject(error);
}
if (originalRequest.url?.includes('/v1/get-account') === true && originalRequest._retry) {
return Promise.reject(error);
}
@@ -138,22 +186,23 @@ if (typeof window !== 'undefined') {
isRefreshing = true;
try {
const response = await refreshToken(
// Edge case: avoid a blank screen if the initial 401 is itself a refresh request.
originalRequest.url?.includes('api/auth/refresh') === true ? true : false,
);
const accountData = await refreshSession();
const token = response?.token ?? '';
// If session is still valid, the cookie is refreshed automatically
// Extract access token if present
const token = accountData?.accessToken ?? accountData?.AccessToken ?? '';
if (token) {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
dispatchTokenUpdatedEvent(token);
processQueue(null, token);
return await axios(originalRequest);
} else if (accountData) {
// Session cookie is valid, just retry
processQueue(null, null);
return await axios(originalRequest);
} else if (window.location.href.includes('share/')) {
console.log(
`Refresh token failed from shared link, attempting request to ${originalRequest.url}`,
);
console.log('Session expired on shared link, attempting request anyway');
} else {
window.location.href = endpoints.loginPage();
}
@@ -195,6 +244,10 @@ export async function getWithPk<T>(url: string): Promise<T> {
return response.data;
}
// Legacy compat: refreshToken calls refreshSession internally
const refreshToken = (retry?: boolean): Promise<t.TRefreshTokenResponse | undefined> =>
refreshSession() as any;
export default {
get: _get,
getResponse: _getResponse,
@@ -206,6 +259,7 @@ export default {
deleteWithOptions: _deleteWithOptions,
patch: _patch,
refreshToken,
refreshSession,
dispatchTokenUpdatedEvent,
setPublishableKey,
getWithPk,
+17 -67
View File
@@ -5,7 +5,6 @@ settings:
excludeLinksFromLockfile: false
overrides:
'@ariakit/react': 0.4.29
zod-to-json-schema: 3.24.6
'@modelcontextprotocol/sdk': 1.22.0
@@ -361,8 +360,8 @@ importers:
client:
dependencies:
'@ariakit/react':
specifier: 0.4.29
version: 0.4.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^0.4.15
version: 0.4.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ariakit/react-core':
specifier: ^0.4.17
version: 0.4.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -936,8 +935,8 @@ importers:
packages/client:
dependencies:
'@ariakit/react':
specifier: 0.4.29
version: 0.4.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^0.4.16
version: 0.4.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ariakit/react-core':
specifier: ^0.4.17
version: 0.4.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1380,18 +1379,9 @@ packages:
peerDependencies:
ajv: '>=8'
'@ariakit/components@0.1.2':
resolution: {integrity: sha512-tvh2P0x1cJnoPXnmDEJwdRk3z7x6cTB8ArctcZdAUXlRg9tuwW/rJoBFJMzD5qMI9CDDlQ3Zctx58HvENw4BYw==}
'@ariakit/core@0.4.18':
resolution: {integrity: sha512-9urEa+GbZTSyredq3B/3thQjTcSZSUC68XctwCkJNH/xNfKN5O+VThiem2rcJxpsGw8sRUQenhagZi0yB4foyg==}
'@ariakit/react-components@0.1.2':
resolution: {integrity: sha512-SM+SPMAVlOZmGAfWNBza+0k9y4mkA5/dJhDoOyhE96cbNARy665uLdwowSJl1JGuFfcZzuzAwGon7f/rYeyfkQ==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@ariakit/react-core@0.4.21':
resolution: {integrity: sha512-rUI9uB/gT3mROFja/ka7/JukkdljIZR3eq3BGiQqX4Ni/KBMDvPK8FvVLnC0TGzWcqNY2bbfve8QllvHzuw4fQ==}
peerDependencies:
@@ -1404,27 +1394,17 @@ packages:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@ariakit/react-store@0.1.2':
resolution: {integrity: sha512-1r1Gn0tqhnOS0LFvHNGzn5/8C5aOANO5vb0Gxh94oR/be4zwCSE2zfQjOjRfpL+BBDhOcProME2+G6UslEJxbg==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@ariakit/react-utils@0.1.2':
resolution: {integrity: sha512-Rnl6D1542Mqu80xK++oUv1JXS0PtNmKXd9nkdud5nyvySiBDTrmPqRW44/D+5GbuZrboreQuY3tPYwKL7a7onQ==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@ariakit/react@0.4.29':
resolution: {integrity: sha512-SLXlsddWHSwfUol4Yi0zULlalNWjzWjpS3zg7B7aaPd64saONQ5ktWf9KMxqBklcpjMLeF2dB9BAHAvpPVdCIQ==}
'@ariakit/react@0.4.21':
resolution: {integrity: sha512-UjP99Y7cWxA5seRECEE0RPZFImkLGFIWPflp65t0BVZwlMw4wp9OJZRHMrnkEkKl5KBE2NR/gbbzwHc6VNGzsA==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@ariakit/store@0.1.2':
resolution: {integrity: sha512-SS7bV4+a+1q9M9i0WV6DD4P/ypRKlCvII8soo2UMe1yuaxZA/Fc0htHe+EZwjJ6TMLjHfHh2TDSnXyrjC7QImA==}
'@ariakit/utils@0.1.2':
resolution: {integrity: sha512-lBJhtBWpKjIck/9i7G8cahvaUgLsyGklI/Pjv+VtY9KTzyuzX5GpRbbLKMS/e1qLnFPS4C3CybYB70b1bVcAkw==}
'@ariakit/react@0.4.23':
resolution: {integrity: sha512-zokuZ7C/pUtFi5x1d/0h5ulLGlJpnPXG1aFKU3F4Sj6sD9uNN/J+fXFsg3sZlWdg7u9ZhBLcjsheLypDjjf6WQ==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
@@ -13741,24 +13721,8 @@ snapshots:
jsonpointer: 5.0.1
leven: 3.1.0
'@ariakit/components@0.1.2':
dependencies:
'@ariakit/store': 0.1.2
'@ariakit/utils': 0.1.2
'@ariakit/core@0.4.18': {}
'@ariakit/react-components@0.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ariakit/components': 0.1.2
'@ariakit/react-store': 0.1.2(react@18.3.1)
'@ariakit/react-utils': 0.1.2(react@18.3.1)
'@ariakit/store': 0.1.2
'@ariakit/utils': 0.1.2
'@floating-ui/dom': 1.7.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@ariakit/react-core@0.4.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ariakit/core': 0.4.18
@@ -13775,31 +13739,17 @@ snapshots:
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.6.0(react@18.3.1)
'@ariakit/react-store@0.1.2(react@18.3.1)':
'@ariakit/react@0.4.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ariakit/react-utils': 0.1.2(react@18.3.1)
'@ariakit/store': 0.1.2
'@ariakit/utils': 0.1.2
react: 18.3.1
use-sync-external-store: 1.6.0(react@18.3.1)
'@ariakit/react-utils@0.1.2(react@18.3.1)':
dependencies:
'@ariakit/store': 0.1.2
'@ariakit/utils': 0.1.2
react: 18.3.1
'@ariakit/react@0.4.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ariakit/react-components': 0.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ariakit/react-core': 0.4.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@ariakit/store@0.1.2':
'@ariakit/react@0.4.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ariakit/utils': 0.1.2
'@ariakit/utils@0.1.2': {}
'@ariakit/react-core': 0.4.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@asamuzakjp/css-color@3.2.0':
dependencies: